¿Por qué es sscanf de glibc mucho más lento que fscanf en Linux?
performance (2)
Parece que sscanf()
glibc explora la cadena fuente para determinar la longitud antes de hacer cualquier otra cosa.
sscanf()
(en stdio-common/sscanf.c
) es esencialmente una envoltura alrededor de una llamada a _IO_vsscanf()
(en libio/iovsscanf.c
). Y una de las primeras cosas que hace _IO_vsscanf()
es inicializar su propia estructura _IO_strfile
llamando a _IO_str_init_static_internal()
(en libio/strops.c
) que calcula la longitud de la cadena si no se proporciona.
Estoy usando GCC 4.8 y glibc 2.19 en un x86_64 Linux.
Mientras jugaba con diferentes métodos de entrada para una pregunta diferente , fscanf
y sscanf
. Específicamente, yo usaría fscanf
en la entrada estándar directamente:
char s[128]; int n;
while (fscanf(stdin, "%127s %d", s, &n) == 2) { }
O primero leería toda la entrada en un búfer y luego atravesaría el búfer con sscanf
. (Leer todo en el búfer lleva una pequeña cantidad de tiempo).
char s[128]; int n;
char const * p = my_data;
for (int b; sscanf(p, "%127s %d%n", s, &n, &b) == 2; p += b) { }
Para mi sorpresa, la versión fscanf
es mucho más rápida. Por ejemplo, el procesamiento de varias decenas de miles de líneas con fscanf
toma este tiempo:
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
Ahora lo mismo con 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
Estaba usando las herramientas de perf de Google para medir esto. Por ejemplo, para 50000 líneas, el código fscanf
requiere aproximadamente 50M ciclos, y el código sscanf
aproximadamente 3300M ciclos. Así que desglosé los sitios de llamadas principales con el perf report
perf record
/ perf report
. Con 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
Y con 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
¡Así que casi todo el tiempo con sscanf
se gasta en rawmemchr
! ¿Por qué es esto? ¿Cómo puede el código fscanf
evitar este costo?
Intenté buscar esto, pero lo mejor que pude encontrar es esta discusión de llamadas de realloc
bloqueadas que no creo que se aplique aquí. También pensé que fscanf
tiene una mejor fscanf
memoria (usando el mismo búfer una y otra vez), pero eso no puede hacer una gran diferencia.
¿Alguien tiene alguna idea en esta extraña discrepancia?
sscanf () convierte la cadena que pasa a un _IO_FILE*
para que la cadena se vea como un "archivo". Esto es así, el mismo _IO_vfscanf () interno se puede usar tanto para una cadena como para un ARCHIVO *.
Sin embargo, como parte de esa conversión, realizada en una función _IO_str_init_static_internal (), llama a __rawmemchr (ptr, ''/0'');
esencialmente una llamada strlen (), en su cadena de entrada. Esta conversión se realiza en cada llamada a sscanf (), y como su búfer de entrada es bastante grande, pasará una buena cantidad de tiempo calculando la longitud de la cadena de entrada.
Crear un ARCHIVO * a partir de la cadena de entrada usando fmemopen () y usar fscanf () podría ser otra alternativa.