Ha habido muchas suposiciones (ligeramente o totalmente) erróneas en los comentarios sobre algunos detalles / antecedentes para esto.
Estás viendo la implementación optimizada de respaldo C optimizada de glibc. (Para ISA que no tienen una implementación de asm escrita a mano) . O una versión anterior de ese código, que todavía está en el árbol fuente de glibc. https://code.woboq.org/userspace/glibc/string/strlen.c.html es un navegador de códigos basado en el árbol glibc git actual. Aparentemente, todavía lo usan algunos objetivos de glibc convencionales, incluido MIPS. (Gracias @zwol).
En ISA populares como x86 y ARM, glibc usa asm escritos a mano
Por lo tanto, el incentivo para cambiar cualquier cosa sobre este código es menor de lo que piensas.
Este código de bithack ( https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord ) no es lo que realmente se ejecuta en su servidor / computadora de escritorio / computadora portátil / teléfono inteligente. Es mejor que un bucle ingenuo byte-a-a-time, pero incluso este bithack es bastante malo en comparación con el asm eficiente para CPU modernas (especialmente x86 donde AVX2 SIMD permite verificar 32 bytes con un par de instrucciones, permitiendo 32 a 64 bytes por reloj haga un ciclo en el bucle principal si los datos están calientes en la caché L1d en las CPU modernas con carga de vector de 2 / reloj y rendimiento de ALU, es decir, para cadenas de tamaño mediano donde la sobrecarga de inicio no domina).
glibc utiliza trucos de enlace dinámico para resolver strlen
una versión óptima para su CPU, por lo que incluso dentro de x86 hay una versión SSE2 (vectores de 16 bytes, línea de base para x86-64) y una versión AVX2 (vectores de 32 bytes).
x86 tiene una transferencia de datos eficiente entre registros vectoriales y de propósito general, lo que lo hace único (?) bueno para usar SIMD para acelerar funciones en cadenas de longitud implícita donde el control de bucle depende de los datos. pcmpeqb
/ pmovmskb
hace posible probar 16 bytes separados a la vez.
glibc tiene una versión AArch64 como esa usando AdvSIMD , y una versión para CPU AArch64 donde los registros vector-> GP bloquean la canalización, por lo que en realidad usa este bithack . Pero utiliza los ceros de recuento para encontrar el byte-dentro del registro una vez que recibe un acierto, y aprovecha los eficientes accesos no alineados de AArch64 después de verificar el cruce de páginas.
También relacionado: ¿Por qué este código es 6.5 veces más lento con las optimizaciones habilitadas? tiene más detalles sobre lo que es rápido versus lento en x86 asm strlen
con un gran búfer y una implementación simple de asm que podría ser bueno para que gcc sepa cómo en línea. (Algunas versiones de gcc imprudentemente en línea, rep scasb
que es muy lenta, o un bithack de 4 bytes a la vez como este. Por lo tanto, la receta en línea de GCC necesita actualización o desactivación).
Asm no tiene "comportamiento indefinido" estilo C ; es seguro acceder a los bytes en la memoria como desee, y una carga alineada que incluya los bytes válidos no puede fallar. La protección de la memoria ocurre con granularidad de página alineada; los accesos alineados más estrechos que eso no pueden cruzar el límite de una página. ¿Es seguro leer más allá del final de un búfer dentro de la misma página en x86 y x64? El mismo razonamiento se aplica al código de máquina que este hack de C consigue que los compiladores creen para una implementación independiente y no en línea de esta función.
Cuando un compilador emite código para llamar a una función no en línea desconocida, debe suponer que la función modifica cualquiera / todas las variables globales y cualquier memoria a la que posiblemente tenga un puntero. es decir, todo excepto los locales que no han tenido su escape de dirección deben estar sincronizados en la memoria durante la llamada. Esto se aplica a las funciones escritas en asm, obviamente, pero también a las funciones de la biblioteca. Si no habilita la optimización del tiempo de enlace, incluso se aplica a unidades de traducción separadas (archivos fuente).
Por qué esto es seguro como parte de glibc pero no de otra manera.
El factor más importante es que esto strlen
no puede alinearse con nada más. No es seguro para eso; contiene UB de alias estricto (lectura de char
datos a través de un unsigned long*
). char*
se le permite alias cualquier otra cosa, pero lo contrario no es cierto .
Esta es una función de biblioteca para una biblioteca compilada por adelantado (glibc). No se alineará con la optimización del tiempo de enlace en las personas que llaman. Esto significa que solo tiene que compilar un código de máquina seguro para una versión independiente de strlen
. No tiene que ser portátil / seguro C.
La biblioteca GNU C solo tiene que compilarse con GCC. Aparentemente no es compatible compilarlo con clang o ICC, a pesar de que admiten extensiones GNU. GCC es un compilador anticipado que convierte un archivo fuente C en un archivo objeto de código de máquina. No es un intérprete, por lo que, a menos que esté en línea en el momento de la compilación, los bytes en la memoria son solo bytes en la memoria. es decir, UB de alias estricto no es peligroso cuando los accesos con diferentes tipos ocurren en diferentes funciones que no se alinean entre sí.
Recuerde que strlen
el comportamiento está definido por el estándar ISO C. Ese nombre de función específicamente es parte de la implementación. Los compiladores como GCC incluso tratan el nombre como una función incorporada a menos que lo use -fno-builtin-strlen
, por lo que strlen("foo")
puede ser una constante de tiempo de compilación 3
. La definición en la biblioteca solo se usa cuando gcc decide emitirle una llamada en lugar de incluir su propia receta o algo así.
Cuando UB no es visible para el compilador en el momento de la compilación, obtienes un código de máquina sensato. El código de la máquina tiene que funcionar para el caso sin UB, e incluso si lo desea , el asm no puede detectar qué tipos utiliza la persona que llama para colocar los datos en la memoria señalada.
Glibc se compila en una biblioteca estática o dinámica independiente que no puede alinearse con la optimización del tiempo de enlace. Los scripts de compilación de glibc no crean bibliotecas estáticas "gordas" que contengan código máquina + representación interna Gcc GIMPLE para la optimización del tiempo de enlace cuando se incorporan a un programa. (es decir libc.a
, no participará en la -flto
optimización del tiempo de enlace en el programa principal). Construir glibc de esa manera sería potencialmente inseguro en los objetivos que realmente usan esto.c
.
De hecho, como comenta @zwol, LTO no se puede usar al construir glibc en sí , debido a un código "frágil" como este que podría romperse si fuera posible la alineación entre los archivos fuente de glibc. (Hay algunos usos internos de strlen
, por ejemplo, tal vez como parte de la printf
implementación)
Esto strlen
hace algunas suposiciones:
CHAR_BIT
es múltiplo de 8 . Verdadero en todos los sistemas GNU. POSIX 2001 incluso garantiza CHAR_BIT == 8
. (Esto parece seguro para sistemas con CHAR_BIT= 16
o 32
, como algunos DSP; el bucle de prólogo no alineado siempre ejecutará 0 iteraciones si sizeof(long) = sizeof(char) = 1
cada puntero siempre está alineado y p & sizeof(long)-1
siempre es cero). Pero si tenía un conjunto de caracteres no ASCII donde los caracteres son 9 o 12 bits de ancho, 0x8080...
es el patrón incorrecto.
- (tal vez)
unsigned long
es de 4 u 8 bytes. O tal vez realmente funcione para cualquier tamaño de unsigned long
hasta 8, y utiliza un assert()
para verificar eso.
Esos dos no son posibles UB, son simplemente no portabilidad para algunas implementaciones de C. Este código es (o fue) parte de la implementación de C en plataformas donde funciona, así que está bien.
El siguiente supuesto es potencial C UB:
- Una carga alineada que contiene bytes válidos no puede fallar , y es segura siempre que ignore los bytes fuera del objeto que realmente desea. (Verdadero en asm en todos los sistemas GNU, y en todas las CPU normales porque la protección de memoria ocurre con granularidad de página alineada. ¿Es seguro leer más allá del final de un búfer dentro de la misma página en x86 y x64? Seguro en C cuando el UB no es visible en el momento de la compilación. Sin alinear, este es el caso aquí. El compilador no puede probar que leer más allá del primero
0
es UB; podría ser una char[]
matriz C que contiene, {1,2,0,3}
por ejemplo)
Ese último punto es lo que hace que sea seguro leer más allá del final de un objeto C aquí. Eso es bastante seguro incluso cuando se alinea con los compiladores actuales porque creo que actualmente no tratan que no se pueda alcanzar una ruta de ejecución. Pero de todos modos, el alias estricto ya es un éxito si alguna vez dejas esto en línea.
Entonces tendría problemas como la vieja memcpy
macro CPP insegura del kernel de Linux que usaba puntero-casting para unsigned long
( gcc, alias estricto e historias de terror ).
Esto strlen
se remonta a la época en la que podía salirse con la suya en general ; solía ser bastante seguro sin la advertencia "solo cuando no está en línea" antes de GCC3.
UB que solo es visible cuando se miran a través de los límites de llamadas / ret no puede hacernos daño. (por ejemplo, llamar a esto en char buf[]
lugar de en una matriz de unsigned long[]
conversión a a const char*
). Una vez que el código de la máquina se establece en piedra, solo se trata de bytes en la memoria. Una llamada de función no en línea tiene que suponer que la persona que llama lee cualquier / toda la memoria.
Escribir esto de forma segura, sin alias estricto UB
El atributo de tipo GCCmay_alias
le da a un tipo el mismo tratamiento de alias-cualquier cosa que char*
. (Sugerido por @KonradBorowsk). Los encabezados GCC actualmente lo usan para tipos de vectores SIMD x86 como __m128i
para que siempre pueda hacerlo de manera segura _mm_loadu_si128( (__m128i*)foo )
. (Consulte ¿Es `reinterpret_cast`ing entre el puntero de vector de hardware y el tipo correspondiente un comportamiento indefinido? Para obtener más detalles sobre lo que esto significa y lo que no significa).
strlen(const char *char_ptr)
{
typedef unsigned long __attribute__((may_alias)) aliasing_ulong;
aliasing_ulong *longword_ptr = (aliasing_ulong *)char_ptr;
for (;;) {
unsigned long ulong = *longword_ptr++; // can safely alias anything
...
}
}
También puede usar aligned(1)
para expresar un tipo con alignof(T) = 1
.
typedef unsigned long __attribute__((may_alias, aligned(1))) unaligned_aliasing_ulong;
Es una forma portátil de expresar una carga de alias en ISOmemcpy
, con la cual los compiladores modernos saben cómo alinearse como una sola instrucción de carga. p.ej
unsigned long longword;
memcpy(&longword, char_ptr, sizeof(longword));
char_ptr += sizeof(longword);
Esto también funciona para cargas no alineadas porque memcpy
funciona como si fuera por char
acceso a la vez. Pero en la práctica, los compiladores modernos entienden memcpy
muy bien.
El peligro aquí es que si GCC no sabe con certeza si char_ptr
está alineado con palabras, no lo alineará en algunas plataformas que podrían no soportar cargas no alineadas en asm. por ejemplo, MIPS antes de MIPS64r6, o ARM anterior. Si recibió una llamada a la función real memcpy
solo para cargar una palabra (y dejarla en otra memoria), eso sería un desastre. A veces, GCC puede ver cuándo el código alinea un puntero. O después del ciclo de char-at-a-time que alcanza un límite ulong podría usar
p = __builtin_assume_aligned(p, sizeof(unsigned long));
Esto no evita la posible UB de leer más allá del objeto, pero con el CCG actual eso no es peligroso en la práctica.
Por qué es necesaria una fuente C optimizada a mano: los compiladores actuales no son lo suficientemente buenos
El asm optimizado a mano puede ser aún mejor cuando desea hasta el último rendimiento para una función de biblioteca estándar ampliamente utilizada. Especialmente para algo así memcpy
, pero también strlen
. En este caso, no sería mucho más fácil usar C con intrínsecos x86 para aprovechar SSE2.
Pero aquí solo estamos hablando de una versión ingenua vs. bithack C sin ninguna característica específica de ISA.
(Creo que podemos tomarlo como un hecho que strlen
se usa lo suficiente como para hacer que funcione lo más rápido posible. Por lo tanto, la pregunta es si podemos obtener un código de máquina eficiente de una fuente más simple. No, no podemos).
GCC y clang actuales no son capaces de auto-vectorizar bucles donde el recuento de iteraciones no se conoce antes de la primera iteración . (por ejemplo, tiene que ser posible verificar si el bucle ejecutará al menos 16 iteraciones antes de ejecutar la primera iteración). Por ejemplo, es posible autovectorizar memcpy (buffer de longitud explícita) pero no strcpy o strlen (cadena de longitud implícita), dada la corriente compiladores
Eso incluye bucles de búsqueda, o cualquier otro bucle con if()break
un contador dependiente de datos .
ICC (compilador de Intel para x86) puede vectorizar automáticamente algunos bucles de búsqueda, pero aún así hace ingenuo byte a la vez para una C simple / ingenua strlen
como la libc de OpenBSD. ( Godbolt ) (De la respuesta de @ Peske ).
Una libc optimizada a mano strlen
es necesaria para el rendimiento con los compiladores actuales . Ir a 1 byte a la vez (con desenrollar quizás 2 bytes por ciclo en CPU superescalares anchas) es patético cuando la memoria principal puede mantenerse con aproximadamente 8 bytes por ciclo, y el caché L1d puede entregar de 16 a 64 por ciclo. (2x cargas de 32 bytes por ciclo en las CPU x86 mainstream modernas desde Haswell y Ryzen. Sin contar AVX512 que puede reducir las velocidades de reloj solo por usar vectores de 512 bits; es por eso que glibc probablemente no tiene prisa por agregar una versión AVX512 . Aunque con vectores de 256 bits, AVX512VL + BW enmascarados comparar en una máscara y ktest
, o kortest
podría hacer strlen
más amigable Hyperthreading mediante la reducción de sus uops / iteración.)
Estoy incluyendo no x86 aquí, esos son los "16 bytes". por ejemplo, la mayoría de las CPU AArch64 pueden hacer al menos eso, creo, y algunas ciertamente más. Y algunos tienen suficiente rendimiento de ejecución para strlen
mantenerse al día con ese ancho de banda de carga.
Por supuesto, los programas que funcionan con cadenas grandes generalmente deben realizar un seguimiento de las longitudes para evitar tener que rehacer la búsqueda de la longitud de las cadenas C de longitud implícita muy a menudo. Pero el rendimiento de corta a media duración todavía se beneficia de las implementaciones escritas a mano, y estoy seguro de que algunos programas terminan usando strlen en cadenas de mediana longitud.