LINUX.ORG.RU

Производительность чтения файла

 , , ,


1

2

Пишу свой супер-пупер производительный парсер на CL под sbcl, GNU/Linux. И давно вот заметил что кусками равного размера чтение из файла идёт быстрее чем если читать поток по символу.


(in-package cl-user)

(defpackage buffering-tests
  (:use :cl))

(in-package buffering-tests)

(defparameter *test-text*
  (merge-pathnames #P"Desktop/buffering-test/text.txt"
                   (user-homedir-pathname)))

(defun count-chars (file)
  (with-open-file (in file)
    (loop :for c = (read-char in nil nil)
          :while c
            :counting 1)))

(defparameter *default-buffer-size*
  (floor (* 4 (expt 2 10))))

(defun count-chars-with-buffering (file
                                   &optional (buffer-size *default-buffer-size*))
  (with-open-file (in file)
    (loop :with buffer = (make-string buffer-size)
          :for e = (read-sequence buffer in)
          :while (plusp e)
          :summing e)))

  
BUFFERING-TESTS> (let (r)
                   (sb-ext:gc :full t)
                   (time (dotimes (i 15)
                           (setf r
                                 (count-chars *test-text*))))
                   r)
Evaluation took:
  1.514 seconds of real time
  1.512376 seconds of total run time (1.490228 user, 0.022148 system)
  99.87% CPU
  3,487,695,298 processor cycles
  63,488 bytes consed
  
6210660
BUFFERING-TESTS> (let (r)
                   (sb-ext:gc :full t)
                   (time (dotimes (i 15)
                           (setf r
                                 (count-chars-with-buffering *test-text*))))
                   r)
Evaluation took:
  0.693 seconds of real time
  0.692603 seconds of total run time (0.669793 user, 0.022810 system)
  100.00% CPU
  1,597,583,804 processor cycles
  323,520 bytes consed
  
6210660
  

Разница есть если даже пытаться сравнять количество итераций:

(defun count-chars-with-buffering (file
                                   &optional (buffer-size *default-buffer-size*))
  (with-open-file (in file)
    (loop :with buffer = (make-string buffer-size)
          :for e = (read-sequence buffer in)
          :while (plusp e)
          :summing
          (loop :repeat e
                :for i :upfrom 0
                :for c = (schar buffer i)
                :when c
                  :counting 1))))
  
BUFFERING-TESTS> (let (r)
                   (sb-ext:gc :full t)
                   (time (dotimes (i 15)
                           (setf r
                                 (count-chars-with-buffering *test-text*))))
                   r)
Evaluation took:
  1.193 seconds of real time
  1.191737 seconds of total run time (1.172685 user, 0.019052 system)
  99.92% CPU
  2,748,553,404 processor cycles
  323,520 bytes consed
  
6210660
BUFFERING-TESTS> 
  

В парсере это привело к созданию прослойки над потоками - объекта, содержащего ссылку на поток, буфер и инфу откуда программе начинать чтение. Остаётся вопрос - для чего так реализовано? Недостающая оптимизация sbcl? В других реализациях других языков тоже такая разница? Понятно что сварганить вышеописанный объект может любой программист в два счёта, но почему бы не сделать это в рамках реализации чтобы не было разницы посимвольно ты читаешь поток или кусками? Почему бы эту абстракцию не взять на себя ядру, у которого и так в IO буфер буфером погоняет?

★★★★★

Чтение по байту медленнее, чем чтение большими кусками. Так везде, даже если ты будешь голыми системными вызовами пользоваться (особенно если ты будешь голыми системными вызовами пользоваться). Потому что цена системного вызова — не ноль. Даже если ты используешь буферизацию из рантайма языка или из библиотеки, обслуживающий код тоже ненулевой, и он сколько-то выполняется. Чем больше ты его вызываешь, тем больше платишь.

Почему бы эту абстракцию не взять на себя ядру

Дык, оно берёт. Иначе просадки скорости были бы жуткими. (Заметно было на Turbo Pascal под DOS. Там если код работы с файлами не использовал BlockRead()/BlockWrite(), скорость была никакая.)

но почему бы не сделать это в рамках реализации чтобы не было разницы посимвольно ты читаешь поток или кусками?

Наверное, могли как-то макросами реализовать, чтобы код буферизации не нужно было вызывать, а он встраивался прямо в твой код. В sbcl же есть макросы?

i-rinat ★★★★★ ()
Последнее исправление: i-rinat (всего исправлений: 3)
Ответ на: комментарий от i-rinat

Наверное, могли как-то макросами реализовать, чтобы код буферизации не нужно было вызывать, а он встраивался прямо в твой код. В sbcl же есть макросы?

Конечно я эти макросы в своём парсере запилил.

ados ★★★★★ ()
Ответ на: комментарий от ados

более медленное посимвольное чтение для чего-то нужно. Вот для чего - ума не приложу.

В сях можно использовать fread() для чтения из файла по одному байту. Можно блоками. Но когда хочется выжать скорости, в дело идут read(). Или readv(), если нужно читать в несколько буферов сразу, и выделять один сплошной буфер не хочется. Или preadv(), если нужно сэкономить на вызове lseek(). Или sendfile(), если нет смысла копировать данные из пространства ядра в юзерспейс только чтобы потом снова копировать в пространство ядра, чтобы послать в сокет. Так дальше можно добраться до работы с DPDK, чтобы не платить за абстракции сокетов.

Но всё-таки иногда удобнее просто через fread() несколько байт прочитать, чтобы не заморачиваться со сложностями там, где это не нужно.

Лисп — не Си. Но не думаю, что он в этом вопросе сильно отличается.

i-rinat ★★★★★ ()
Последнее исправление: i-rinat (всего исправлений: 1)
Ответ на: комментарий от ados

открыть файл, прочитать первый байт

Это зависит от того, какой файл. Просто файл на блочном устройстве - бессмысленно, т.к. читается всегда минимум блок. А вот если читать из символьного устройства (например с клавиатуры), то там это оправдано (и даже правильнее читать именно побайтно).

no-such-file ★★★★★ ()

чтобы не было разницы посимвольно ты читаешь поток или кусками

И как бы ты читал ввод с клавиатуры? Ждал бы пока пользователь введёт 4k байт?

no-such-file ★★★★★ ()