21

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))
19

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))
4
  • Yes, that reduced it from 1.35 seconds to 0.003559! Thank you very much -- greatly appreciated! :) – lawlist Nov 23 '14 at 8:10
  • 7
    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. – 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)
1
  • 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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.