torax tecnicas tecnica respiratorio respiratorias respiratoria respiracion manuales indicaciones forzada fisioterapia espiracion drenaje ciclo autogeno activo acbt c++ c++11 optimization compiler-optimization strict-aliasing

c++ - respiratorio - tecnicas manuales de fisioterapia respiratoria



El uso de este puntero provoca una desoptimización extraña en el bucle dinámico (3)

El alias de puntero parece ser el problema, irónicamente entre this y this->target . El compilador está teniendo en cuenta la posibilidad bastante obscena que inicializó:

this->target = &this

En ese caso, escribir en this->target[0] alteraría el contenido de this (y por lo tanto, this-> target).

El problema de alias de memoria no está restringido a lo anterior. En principio, cualquier uso de this->target[XX] dado un valor (in) apropiado de XX podría apuntar a this .

Estoy mejor versado en C, donde esto puede remediarse declarando variables de puntero con la palabra clave __restrict__.

Recientemente me encontré con una desoptimización extraña (o más bien perdí la oportunidad de optimización).

Considere esta función para desempaquetar de manera eficiente conjuntos de enteros de 3 bits a enteros de 8 bits. Descomprime 16 ints en cada iteración de bucle:

void unpack3bit(uint8_t* target, char* source, int size) { while(size > 0){ uint64_t t = *reinterpret_cast<uint64_t*>(source); target[0] = t & 0x7; target[1] = (t >> 3) & 0x7; target[2] = (t >> 6) & 0x7; target[3] = (t >> 9) & 0x7; target[4] = (t >> 12) & 0x7; target[5] = (t >> 15) & 0x7; target[6] = (t >> 18) & 0x7; target[7] = (t >> 21) & 0x7; target[8] = (t >> 24) & 0x7; target[9] = (t >> 27) & 0x7; target[10] = (t >> 30) & 0x7; target[11] = (t >> 33) & 0x7; target[12] = (t >> 36) & 0x7; target[13] = (t >> 39) & 0x7; target[14] = (t >> 42) & 0x7; target[15] = (t >> 45) & 0x7; source+=6; size-=6; target+=16; } }

Aquí está el ensamblado generado para partes del código:

... 367: 48 89 c1 mov rcx,rax 36a: 48 c1 e9 09 shr rcx,0x9 36e: 83 e1 07 and ecx,0x7 371: 48 89 4f 18 mov QWORD PTR [rdi+0x18],rcx 375: 48 89 c1 mov rcx,rax 378: 48 c1 e9 0c shr rcx,0xc 37c: 83 e1 07 and ecx,0x7 37f: 48 89 4f 20 mov QWORD PTR [rdi+0x20],rcx 383: 48 89 c1 mov rcx,rax 386: 48 c1 e9 0f shr rcx,0xf 38a: 83 e1 07 and ecx,0x7 38d: 48 89 4f 28 mov QWORD PTR [rdi+0x28],rcx 391: 48 89 c1 mov rcx,rax 394: 48 c1 e9 12 shr rcx,0x12 398: 83 e1 07 and ecx,0x7 39b: 48 89 4f 30 mov QWORD PTR [rdi+0x30],rcx ...

Se ve bastante eficiente. Simplemente un shift right seguido de una and , y luego una store al búfer de target . Pero ahora, mira lo que sucede cuando cambio la función a un método en una estructura:

struct T{ uint8_t* target; char* source; void unpack3bit( int size); }; void T::unpack3bit(int size) { while(size > 0){ uint64_t t = *reinterpret_cast<uint64_t*>(source); target[0] = t & 0x7; target[1] = (t >> 3) & 0x7; target[2] = (t >> 6) & 0x7; target[3] = (t >> 9) & 0x7; target[4] = (t >> 12) & 0x7; target[5] = (t >> 15) & 0x7; target[6] = (t >> 18) & 0x7; target[7] = (t >> 21) & 0x7; target[8] = (t >> 24) & 0x7; target[9] = (t >> 27) & 0x7; target[10] = (t >> 30) & 0x7; target[11] = (t >> 33) & 0x7; target[12] = (t >> 36) & 0x7; target[13] = (t >> 39) & 0x7; target[14] = (t >> 42) & 0x7; target[15] = (t >> 45) & 0x7; source+=6; size-=6; target+=16; } }

Pensé que el ensamblado generado debería ser el mismo, pero no lo es. Aquí hay una parte de esto:

... 2b3: 48 c1 e9 15 shr rcx,0x15 2b7: 83 e1 07 and ecx,0x7 2ba: 88 4a 07 mov BYTE PTR [rdx+0x7],cl 2bd: 48 89 c1 mov rcx,rax 2c0: 48 8b 17 mov rdx,QWORD PTR [rdi] // Load, BAD! 2c3: 48 c1 e9 18 shr rcx,0x18 2c7: 83 e1 07 and ecx,0x7 2ca: 88 4a 08 mov BYTE PTR [rdx+0x8],cl 2cd: 48 89 c1 mov rcx,rax 2d0: 48 8b 17 mov rdx,QWORD PTR [rdi] // Load, BAD! 2d3: 48 c1 e9 1b shr rcx,0x1b 2d7: 83 e1 07 and ecx,0x7 2da: 88 4a 09 mov BYTE PTR [rdx+0x9],cl 2dd: 48 89 c1 mov rcx,rax 2e0: 48 8b 17 mov rdx,QWORD PTR [rdi] // Load, BAD! 2e3: 48 c1 e9 1e shr rcx,0x1e 2e7: 83 e1 07 and ecx,0x7 2ea: 88 4a 0a mov BYTE PTR [rdx+0xa],cl 2ed: 48 89 c1 mov rcx,rax 2f0: 48 8b 17 mov rdx,QWORD PTR [rdi] // Load, BAD! ...

Como puede ver, introdujimos una load redundante adicional de la memoria antes de cada turno ( mov rdx,QWORD PTR [rdi] ). Parece que el puntero de target (que ahora es un miembro en lugar de una variable local) siempre debe recargarse antes de almacenarlo. Esto ralentiza el código considerablemente (alrededor del 15% en mis mediciones).

Primero, pensé que tal vez el modelo de memoria de C ++ exige que un puntero miembro no se almacene en un registro, sino que deba volverse a cargar, pero esto parecía una elección incómoda, ya que haría imposible muchas optimizaciones viables. Así que me sorprendió mucho que el compilador no almacenara el target en un registro aquí.

Intenté almacenar en caché el puntero del miembro en una variable local:

void T::unpack3bit(int size) { while(size > 0){ uint64_t t = *reinterpret_cast<uint64_t*>(source); uint8_t* target = this->target; // << ptr cached in local variable target[0] = t & 0x7; target[1] = (t >> 3) & 0x7; target[2] = (t >> 6) & 0x7; target[3] = (t >> 9) & 0x7; target[4] = (t >> 12) & 0x7; target[5] = (t >> 15) & 0x7; target[6] = (t >> 18) & 0x7; target[7] = (t >> 21) & 0x7; target[8] = (t >> 24) & 0x7; target[9] = (t >> 27) & 0x7; target[10] = (t >> 30) & 0x7; target[11] = (t >> 33) & 0x7; target[12] = (t >> 36) & 0x7; target[13] = (t >> 39) & 0x7; target[14] = (t >> 42) & 0x7; target[15] = (t >> 45) & 0x7; source+=6; size-=6; this->target+=16; } }

Este código también produce el ensamblador "bueno" sin tiendas adicionales. Entonces, supongo: el compilador no puede elevar la carga de un puntero miembro de una estructura, por lo que dicho "puntero activo" siempre debe almacenarse en una variable local.

  • Entonces, ¿por qué el compilador no puede optimizar estas cargas?
  • ¿Es el modelo de memoria C ++ lo que prohíbe esto? ¿O es simplemente una deficiencia de mi compilador?
  • ¿Es mi suposición correcta o cuál es la razón exacta por la que no se puede realizar la optimización?

El compilador en uso fue g++ 4.8.2-19ubuntu1 con optimización g++ 4.8.2-19ubuntu1 . También probé clang++ 3.4-1ubuntu3 con resultados similares: Clang incluso puede vectorizar el método con el puntero de target local. Sin embargo, el uso del puntero this->target produce el mismo resultado: una carga adicional del puntero antes de cada tienda.

Verifiqué el ensamblador de algunos métodos similares y el resultado es el mismo: parece que un miembro de this siempre tiene que recargarse antes de una tienda, incluso si tal carga simplemente se puede elevar fuera del bucle. Tendré que volver a escribir una gran cantidad de código para deshacerme de estos almacenes adicionales, principalmente almacenando el puntero en una variable local que se declara sobre el código activo. Pero siempre pensé que jugar con detalles como el almacenamiento en caché de un puntero en una variable local seguramente calificaría para la optimización prematura en estos días en que los compiladores se han vuelto tan inteligentes. Pero parece que estoy equivocado aquí . El almacenamiento en caché de un puntero de miembro en un bucle activo parece ser una técnica de optimización manual necesaria.


El problema aquí es el alias estricto que dice que se nos permite alias a través de un char * y eso evita la optimización del compilador en su caso. No se nos permite alias a través de un puntero de un tipo diferente que sería un comportamiento indefinido, normalmente en SO vemos este problema en el que los usuarios intentan alias a través de tipos de puntero incompatibles .

Parece razonable implementar uint8_t como un carácter sin signo y si miramos cstdint en Coliru , incluye stdint.h que typedefs uint8_t de la siguiente manera:

typedef unsigned char uint8_t;

Si usó otro tipo que no sea char, el compilador debería poder optimizar.

Esto está cubierto en el borrador del estándar C ++ sección 3.10 Valores y valores que dice:

Si un programa intenta acceder al valor almacenado de un objeto a través de un valor gl diferente de uno de los siguientes tipos, el comportamiento es indefinido

e incluye la siguiente viñeta:

  • un tipo de carácter char o unsigned.

Tenga en cuenta que publiqué un comentario sobre posibles soluciones alternativas en una pregunta que pregunta cuándo es uint8_t ≠ unsigned char? y la recomendación fue:

La solución trivial, sin embargo, es usar la palabra clave restrict, o copiar el puntero a una variable local cuya dirección nunca se toma para que el compilador no tenga que preocuparse por si los objetos uint8_t pueden alias.

Dado que C ++ no admite la palabra clave restrictiva , debe confiar en la extensión del compilador, por ejemplo, gcc usa __restrict__, por lo que no es totalmente portátil, pero la otra sugerencia debería serlo.


Las reglas de alias estrictas permiten que char* alias cualquier otro puntero. Entonces this->target puede alias con this , y en su método de código, la primera parte del código,

target[0] = t & 0x7; target[1] = (t >> 3) & 0x7; target[2] = (t >> 6) & 0x7;

es de hecho

this->target[0] = t & 0x7; this->target[1] = (t >> 3) & 0x7; this->target[2] = (t >> 6) & 0x7;

ya que this puede modificarse cuando modifica this->target contenido this->target .

Una vez que this->target se almacena en caché en una variable local, el alias ya no es posible con la variable local.