Подтвердить что ты не робот

Почему glibc sscanf значительно медленнее, чем fscanf для Linux?

Я использую GCC 4.8 и glibc 2.19 на Linux x86_64.

Во время игры с различными методами ввода для другого вопроса я сравнивал fscanf и sscanf. В частности, я бы либо использовал fscanf на стандартном входе напрямую:

char s[128]; int n;

while (fscanf(stdin, "%127s %d", s, &n) == 2) { }

Или я сначала прочитал бы весь ввод в буфер, а затем переместил бы буфер с помощью sscanf. (Чтение всего в буфер занимает крошечное количество времени.)

char s[128]; int n;
char const * p = my_data;

for (int b; sscanf(p, "%127s %d%n", s, &n, &b) == 2; p += b) { }

К моему удивлению, версия fscanf намного быстрее. Например, обработка seveal десятков тысяч строк с fscanf занимает много времени:

10000       0.003927487 seconds time elapsed
20000       0.006860206 seconds time elapsed
30000       0.007933329 seconds time elapsed
40000       0.012881912 seconds time elapsed
50000       0.013516816 seconds time elapsed
60000       0.015670432 seconds time elapsed
70000       0.017393129 seconds time elapsed
80000       0.019837480 seconds time elapsed
90000       0.023925753 seconds time elapsed

Теперь то же самое с sscanf:

10000       0.035864643 seconds time elapsed
20000       0.127150772 seconds time elapsed
30000       0.319828373 seconds time elapsed
40000       0.611551668 seconds time elapsed
50000       0.919187459 seconds time elapsed
60000       1.327831544 seconds time elapsed
70000       1.809843039 seconds time elapsed
80000       2.354809588 seconds time elapsed
90000       2.970678416 seconds time elapsed

Я использовал инструменты Google perf для измерения этого. Например, для 50000 строк код fscanf требует около 50 М циклов, а код sscanf - около 3300 М циклов. Поэтому я сломал верхние сайты вызовов с помощью perf record/perf report. С помощью fscanf:

 35.26%  xf  libc-2.19.so         [.] _IO_vfscanf
 23.91%  xf  [kernel.kallsyms]    [k] 0xffffffff8104f45a
  8.93%  xf  libc-2.19.so         [.] _int_malloc

И с sscanf:

 98.22%  xs  libc-2.19.so         [.] rawmemchr
  0.68%  xs  libc-2.19.so         [.] _IO_vfscanf
  0.38%  xs  [kernel.kallsyms]    [k] 0xffffffff8104f45a

Таким образом, почти все время с sscanf проводится в rawmemchr! Почему это? Как код fscanf позволяет избежать этой стоимости?

Я попытался найти это, но лучшее, что я мог придумать, это это обсуждение заблокированных realloc вызовов, t думаю здесь. Я также думал, что fscanf имеет лучшую локальность памяти (используя один и тот же буфер снова и снова), но это не может сделать такую ​​большую разницу.

Есть ли у кого-нибудь идеи в этом странном несоответствии?

4b9b3361

Ответ 1

sscanf() преобразует строку, в которую вы проходите, в _IO_FILE*, чтобы строка выглядела как "файл". Это значит, что внутренняя _IO_vfscanf() может использоваться как для строки, так и для файла FILE *.

Однако, как часть этого преобразования, выполненного в функции _IO_str_init_static_internal(), он вызывает __rawmemchr (ptr, '\0'); по существу вызов strlen() в вашей строке ввода. Это преобразование выполняется при каждом вызове sscanf(), и поскольку ваш входной буфер довольно велик, он потратит достаточное количество времени на вычисление длины входной строки.

Создание FILE * из входной строки с использованием fmemopen() и использование fscanf() может быть другой альтернативой.

Ответ 2

Похоже, glibc sscanf() сканирует исходную строку для длины, прежде чем делать что-либо еще.

sscanf()stdio-common/sscanf.c) по существу является оберткой вокруг вызова _IO_vsscanf()libio/iovsscanf.c). И одна из первых вещей, которые выполняет _IO_vsscanf(), инициализирует свою собственную структуру _IO_strfile, вызывая _IO_str_init_static_internal()libio/strops.c), которая вычисляет длину строки, если она не указана.