¿Por qué el acceso desalineado a la memoria mmaped algunas veces segfault en AMD64?

Tengo esta pieza de código que segfaults cuando se ejecuta en Ubuntu 14.04 en una CPU compatible con AMD64:

#include  #include  #include  int main() { uint32_t sum = 0; uint8_t *buffer = mmap(NULL, 1<<18, PROT_READ, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); uint16_t *p = (buffer + 1); int i; for (i=0;i<14;++i) { //printf("%d\n", i); sum += p[i]; } return sum; } 

Esto solo segfaults si la memoria se asigna usando mmap . Si uso malloc , un buffer en la stack, o una variable global, no segfault.

Si disminuyo el número de iteraciones del ciclo a algo menos que 14, ya no se segmenta. Y si imprimo el índice de matriz desde dentro del bucle, ya no se segmenta.

¿Por qué la memoria no alineada accede segfault en una CPU que puede acceder a direcciones desalineadas, y por qué solo bajo tales circunstancias específicas?

gcc4.8 crea un prólogo que intenta alcanzar un límite de alineación, pero asume que uint16_t *p está alineado en 2 bytes , es decir, que cierto número de iteraciones escalares harán que el puntero se alinee con 16 bytes.

No creo que gcc haya tenido la intención de soportar punteros desalineados en x86, simplemente funcionaba para tipos no atómicos sin auto-vectorización. Definitivamente es un comportamiento indefinido en ISO C usar un puntero a uint16_t con una uint16_t menor que alignof(uint16_t)=2 . GCC no advierte cuando puede ver que se rompe la regla en el momento de la comstackción, y realmente hace código de trabajo (para malloc donde conoce la alineación mínima de valor de retorno), pero eso es presumiblemente solo un accidente de las partes internas de gcc , y no debe tomarse como una indicación de “apoyo”.


Pruebe con -O3 -fno-tree-vectorize o -O2 . Si mi explicación es correcta, eso no segmentará la falla, ya que solo usará cargas escalares (que, como dices en x86, no tienen requisitos de alineación).


gcc sabe que malloc devuelve una memoria alineada de 16 bytes en este destino (x86-64 Linux, donde maxalign_t tiene 16 bytes de ancho porque el long double tiene relleno en 16 bytes en el x86-64 System V ABI). Ve lo que estás haciendo y usa movdqu .

Pero gcc no trata a mmap como una función incorporada, por lo que no sabe que devuelve memoria alineada con la página, y aplica su estrategia habitual de auto-vectorización que aparentemente asume que uint16_t *p está alineado a 2 bytes, por lo que puede usar movdqa después de manejar la desalineación. Su puntero está desalineado y viola esta suposición.

(Me pregunto si los encabezados glibc más nuevos usan __attribute__((alloc_align(4096))) para marcar el valor de retorno de mmap como alineado. Eso sería una buena idea, y probablemente te habría dado el mismo código-gen que para malloc )


en una CPU que puede acceder sin alinear

SSE2 movdqa segfaults en desalineado, y sus elementos están desalineados por lo que tiene la situación inusual donde ningún elemento de matriz comienza en un límite de 16 bytes.

SSE2 es la línea base para x86-64, por lo que gcc lo usa.


Ubuntu 14.04LTS usa gcc4.8.2 (Tema desactivado: que es viejo y obsoleto, peor código-gen en muchos casos que gcc5.4 o gcc6.4 especialmente cuando se auto-vectoriza. Ni siquiera reconoce -march=haswell .)

14 es el umbral mínimo para que la heurística de gcc decida auto-vectorizar su bucle en esta función , con las -O3 -mtune y no -march o -mtune .

Puse tu código en Godbolt , y esta es la parte relevante de main :

  call mmap # lea rdi, [rax+1] # p, mov rdx, rax # buffer, mov rax, rdi # D.2507, p and eax, 15 # D.2507, shr rax ##### rax>>=1 discards the low byte, assuming it's zero neg rax # D.2507 mov esi, eax # prolog_loop_niters.7, D.2507 and esi, 7 # prolog_loop_niters.7, je .L2 # .L2 leads directly to a MOVDQA xmm2, [rdx+1] 

Se da cuenta (con este bloque de código) de cuántas iteraciones escalares se deben hacer antes de llegar a MOVDQA, pero ninguna de las rutas de código conduce a un bucle MOVDQU. es decir, gcc no tiene una ruta de código para manejar el caso donde p es impar.


Pero el código-gen para malloc se ve así:

  call malloc # movzx edx, WORD PTR [rax+17] # D.2497, MEM[(uint16_t *)buffer_5 + 17B] movzx ecx, WORD PTR [rax+27] # D.2497, MEM[(uint16_t *)buffer_5 + 27B] movdqu xmm2, XMMWORD PTR [rax+1] # tmp91, MEM[(uint16_t *)buffer_5 + 1B] 

Tenga en cuenta el uso de movdqu . Hay algunas cargas movzx más escalares mezcladas: 8 de las 14 iteraciones totales se hacen SIMD, y las 6 restantes con escalar. Esta es una optimización perdida: podría hacer fácilmente otras 4 con una carga movq , especialmente porque llena un vector XMM después de desempaquetar con cero para obtener elementos uint32_t antes de agregar.

(Hay otras optimizaciones perdidas, como usar pmaddwd con un multiplicador de 1 para agregar pares horizontales de palabras a los elementos de dword).


Código de seguridad con punteros desalineados:

Si desea escribir un código que use punteros desalineados, puede hacerlo correctamente en ISO C usando memcpy . En los objectives con soporte de carga desalineado eficiente (como x86), los comstackdores modernos seguirán utilizando simplemente una carga escalar simple en un registro, exactamente como desreferenciar el puntero. Pero cuando se auto-vectoriza, gcc no asumirá que un puntero alineado se alinea con los límites del elemento y usará cargas desalineadas.

memcpy es cómo expresas una carga / tienda desalineada en ISO C / C ++.

 #include  int sum(int *p) { int sum=0; for (int i=0 ; i<10001 ; i++) { // sum += p[i]; int tmp; #ifdef USE_ALIGNED tmp = p[i]; // normal dereference #else memcpy(&tmp, &p[i], sizeof(tmp)); // unaligned load #endif sum += tmp; } return sum; } 

Con gcc7.2 -O3 -DUSE_ALIGNED , obtenemos el escalar habitual hasta un límite de alineación, luego un bucle vectorial: ( Godbolt compiler explorer )

 .L4: # gcc7.2 normal dereference add eax, 1 paddd xmm0, XMMWORD PTR [rdx] add rdx, 16 cmp ecx, eax ja .L4 

Pero con memcpy , obtenemos auto-vectorización con una carga desalineada (sin intro / outro para manejar la alineación), a diferencia de la preferencia normal de gcc:

 .L2: # gcc7.2 memcpy for an unaligned pointer movdqu xmm2, XMMWORD PTR [rdi] add rdi, 16 cmp rax, rdi # end_pointer != pointer paddd xmm0, xmm2 jne .L2 # -mtune=generic still doesn't optimize for macro-fusion of cmp/jcc :( # hsum into EAX, then the final odd scalar element: add eax, DWORD PTR [rdi+40000] # this is how memcpy compiles for normal scalar code, too. 

En el caso del OP, simplemente organizar la alineación de los punteros es una mejor opción. Evita las divisiones de la línea de caché para el código escalar (o para vectorizar la forma en que lo hace gcc). No cuesta una gran cantidad de memoria o espacio extra, y el diseño de datos en la memoria no es fijo.

Pero a veces eso no es una opción. memcpy optimiza bastante confiablemente completamente con gcc / clang moderno cuando copia todos los bytes de un tipo primitivo. es decir, solo una carga o almacenamiento, ninguna llamada de función y ningún rebote a una ubicación de memoria adicional. Incluso en -O0 , esta simple memcpy enmarca sin llamada de función, pero por supuesto tmp no se optimiza.

De todos modos, verifique el asm generado por el comstackdor si le preocupa que no se optimice en un caso más complicado o con comstackdores diferentes. Por ejemplo, ICC18 no auto-vectoriza la versión usando memcpy.

uint64_t tmp=0; y luego memcpy sobre los 3 bytes bajos se comstack en una copia real en la memoria y se vuelve a cargar, por lo que no es una buena forma de express la extensión cero de los tipos de tamaño impar, por ejemplo.