22

The function line-number-at-pos (when repeated about 50 times) is causing a noticeable slow-down in semi-large buffers -- e.g., 50,000 lines -- when point is near the end of the buffer. By slow-down, I mean a combined total of about 1.35 seconds.

Instead of using a 100% elisp funciton to count-lines and goto the top of the buffer, I'd be interested in a hybrid method that taps into the built-in C abilities responsible for the line number appearing on the mode-line. The line-number that appears on the mode-line occurs at light speed, regardless of the size of the buffer.


Here is a test function:

(defmacro measure-time (&rest body)
"Measure the time it takes to evaluate BODY.
http://lists.gnu.org/archive/html/help-gnu-emacs/2008-06/msg00087.html"
  `(let ((time (current-time)))
     ,@body
     (message "%.06f" (float-time (time-since time)))))

(measure-time
  (let* (
      line-numbers
      (window-start (window-start))
      (window-end (window-end)))
    (save-excursion
      (goto-char window-end)
      (while
        (re-search-backward "\n" window-start t)
        (push (line-number-at-pos) line-numbers)))
    line-numbers))
lawlist
  • 18,826
  • 5
  • 37
  • 118

3 Answers3

20

Try

(string-to-number (format-mode-line "%l"))

You can extract other information using %-Constructs described in the Emacs Lisp Manual.

Caveat:

In addition to limitations pointed out by wasamasa and Stefan (see comments below) this does not work for buffers that are not displayed.

Try this:

(with-temp-buffer
  (dotimes (i 10000)
    (insert (format "%d\n" i)))
  (string-to-number (format-mode-line "%l")))

and compare to

(with-temp-buffer
  (dotimes (i 10000)
    (insert (format "%d\n" i)))
  (line-number-at-pos))
Constantine
  • 9,072
  • 1
  • 34
  • 49
  • Yes, that reduced it from 1.35 seconds to 0.003559! Thank you very much -- greatly appreciated! :) – lawlist Nov 23 '14 at 08:10
  • 8
    Be aware that this method will give you "??" for lines exceeding `line-number-display-limit-width` which is set to a value of 200 per default as I found out [here](http://emacs.stackexchange.com/a/3827/10). – wasamasa Nov 23 '14 at 11:29
  • 3
    IIRC the result may also be unreliable if there have been modifications in the buffer since the last redisplay. – Stefan Nov 23 '14 at 14:51
  • I believe it would be necessary to modify the tests in the answer such that the second letter `i` is replaced with `(string-to-number (format-mode-line "%l"))` for the first test, and the second letter `i` is replaced with `(line-number-at-pos)` for the second test. – lawlist Feb 16 '16 at 21:31
5

nlinum.el uses the following:

(defvar nlinum--line-number-cache nil)
(make-variable-buffer-local 'nlinum--line-number-cache)

;; We could try and avoid flushing the cache at every change, e.g. with:
;;   (defun nlinum--before-change (start _end)
;;     (if (and nlinum--line-number-cache
;;              (< start (car nlinum--line-number-cache)))
;;         (save-excursion (goto-char start) (nlinum--line-number-at-pos))))
;; But it's far from clear that it's worth the trouble.  The current simplistic
;; approach seems to be good enough in practice.

(defun nlinum--after-change (&rest _args)
  (setq nlinum--line-number-cache nil))

(defun nlinum--line-number-at-pos ()
  "Like `line-number-at-pos' but sped up with a cache."
  ;; (assert (bolp))
  (let ((pos
         (if (and nlinum--line-number-cache
                  (> (- (point) (point-min))
                     (abs (- (point) (car nlinum--line-number-cache)))))
             (funcall (if (> (point) (car nlinum--line-number-cache))
                          #'+ #'-)
                      (cdr nlinum--line-number-cache)
                      (count-lines (point) (car nlinum--line-number-cache)))
           (line-number-at-pos))))
    ;;(assert (= pos (line-number-at-pos)))
    (setq nlinum--line-number-cache (cons (point) pos))
    pos))

with the following extra config in the mode function:

(add-hook 'after-change-functions #'nlinum--after-change nil t)
Stefan
  • 26,154
  • 3
  • 46
  • 84
  • 1
    Ah ... I was just thinking about your library earlier this morning. The `line-number-at-pos` could be replaced with the answer by Constantine, and that would speed up your library even more than it already its -- especially in large buffers. `count-lines` should also be fixed using the method by Constantine. I was even thinking of sending in a suggest-box submission to the report-emacs-bug hotline to fix those functions. – lawlist Nov 23 '14 at 17:12
1

Revisiting this old topic, (string-to-number (format-mode-line "%l") still holds up quite well against (line-number-at-position). I found it is also highly cached for "nearby" line positions. For example, operating on /usr/dict/words (236k lines here):

(let* ((buf (get-buffer "words"))
       (win (get-buffer-window buf))
       (len (buffer-size buf))
       (off 0)
       (cnt 20000)
       (step (floor (/ (float len) cnt))) line)
  (set-window-point win 0)
  (redisplay) ; we must start at the top; see note [1]
  (measure-time
   (dotimes (i cnt)
     (set-window-point win (cl-incf off (+ step (random 5))));(random len))
     (setq line (string-to-number (format-mode-line "%l" 0 win)))))
  (message "Final line: %d (step %d cnt %d)" line step cnt))

This takes about 20s to run through, visiting 20,000 positions spanning the entire file in order. If instead you just look in the vicinity of a position (here ±5000 characters):

(let* ((buf (get-buffer "words"))
       (win (get-buffer-window buf))
       (len (buffer-size buf))
       (off (/ len 2))
       (cnt 20000)
       (step (floor (/ (float len) cnt))) line)
  (set-window-point win off)
  (redisplay) ; we must start at the top
  (measure-time
   (dotimes (i cnt)
     (let ((pos (+ off (- (random 10000) 5000))))
       (set-window-point win pos);(random len))
       (setq line (string-to-number (format-mode-line "%l" 0 win))))))
  (message "Final line: %d (step %d cnt %d)" line step cnt))

This only takes ~1/5s or so! So (as you'd expect) it's highly optimized for scrolling to nearby locations (vs jumping across the entire file randomly).

In contrast:

(let* ((buf (get-buffer "words"))
       (win (get-buffer-window buf))
       (len (buffer-size buf))
       (off 0)
       (cnt 1000)
       (step (floor (/ (float len) cnt))) line)
    (measure-time
     (dotimes (i cnt)
       (with-current-buffer buf
     (setq line (line-number-at-pos (cl-incf off step))))))
  (message "Final line: %d" line))

takes over 10s (for 20x fewer positions) each and every time. So to summarize, format-mode-line is ~10x faster than line-number-at-pos, on a full and fast run through a long file. And for nearby positions, it gives near instantaneous results through local caching.

[1] Before I brought the point back to the top and redisplayed, subsequent full-file runs were taking << 1 second. At first I thought this was some miraculous internal caching of format-mode-line, but then I noticed that this only happened if you left point at the end of the buffer. In that case set-window-point doesn't actually move point over such long distances, and format-mode-line just quickly returns the same line, over and over.

JDS
  • 21
  • 3