c++ gcc compilation clang compiler-optimization

c++ - Comportamiento inconsistente de la optimización del compilador de la cadena no utilizada



gcc compilation (2)

Esto se debe a la pequeña optimización de cadenas. Cuando los datos de la cadena tienen menos de o 16 caracteres, incluido el terminador nulo, se almacenan en un búfer local para el objeto std::string sí. De lo contrario, asigna memoria en el montón y almacena los datos allí.

La primera cadena "ABCDEFGHIJKLMNO" más el terminador nulo es exactamente del tamaño 16. Agregar "P" hace que exceda el búfer, por lo que se llama internamente a la new , lo que inevitablemente conduce a una llamada al sistema. El compilador puede optimizar algo si es posible asegurarse de que no haya efectos secundarios. Probablemente, una llamada al sistema hace que sea imposible hacer esto; por contraposición, el cambio de un búfer local al objeto en construcción permite tal análisis de efectos secundarios.

El rastreo del búfer local en libstdc ++, versión 9.1, revela estas partes de bits/basic_string.h :

template<typename _CharT, typename _Traits, typename _Alloc> class basic_string { // ... enum { _S_local_capacity = 15 / sizeof(_CharT) }; union { _CharT _M_local_buf[_S_local_capacity + 1]; size_type _M_allocated_capacity; }; // ... };

que le permite detectar el tamaño del búfer local _S_local_capacity y el búfer local ( _M_local_buf ). Cuando el constructor activa basic_string::_M_construct se llama, tiene en bits/basic_string.tcc :

void _M_construct(_InIterator __beg, _InIterator __end, ...) { size_type __len = 0; size_type __capacity = size_type(_S_local_capacity); while (__beg != __end && __len < __capacity) { _M_data()[__len++] = *__beg; ++__beg; }

donde el buffer local se llena con su contenido. Justo después de esta parte, llegamos a la rama donde se ha agotado la capacidad local: se asigna un nuevo almacenamiento (a través de la asignación en M_create ), el búfer local se copia en el nuevo almacenamiento y se llena con el resto del argumento de inicialización:

while (__beg != __end) { if (__len == __capacity) { // Allocate more space. __capacity = __len + 1; pointer __another = _M_create(__capacity, __len); this->_S_copy(__another, _M_data(), __len); _M_dispose(); _M_data(__another); _M_capacity(__capacity); } _M_data()[__len++] = *__beg; ++__beg; }

Como nota al margen, la optimización de cadenas pequeñas es un tema por sí solo. Para tener una idea de cómo ajustar los bits individuales puede hacer una diferencia a gran escala, recomiendo esta charla . También menciona cómo la implementación std::string que viene con gcc (libstdc ++) funciona y cambió en el pasado para que coincida con las versiones más nuevas del estándar.

Tengo curiosidad por qué el siguiente código:

#include <string> int main() { std::string a = "ABCDEFGHIJKLMNO"; }

Cuando se compila con -O3 obtiene el siguiente código:

main: # @main xor eax, eax ret

(Entiendo perfectamente que no hay necesidad de usar a no utilizado, a lo que el compilador puede omitirlo completamente del código generado)

Sin embargo el siguiente programa:

#include <string> int main() { std::string a = "ABCDEFGHIJKLMNOP"; // <-- !!! One Extra P }

rendimientos

main: # @main push rbx sub rsp, 48 lea rbx, [rsp + 32] mov qword ptr [rsp + 16], rbx mov qword ptr [rsp + 8], 16 lea rdi, [rsp + 16] lea rsi, [rsp + 8] xor edx, edx call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_create(unsigned long&, unsigned long) mov qword ptr [rsp + 16], rax mov rcx, qword ptr [rsp + 8] mov qword ptr [rsp + 32], rcx movups xmm0, xmmword ptr [rip + .L.str] movups xmmword ptr [rax], xmm0 mov qword ptr [rsp + 24], rcx mov rax, qword ptr [rsp + 16] mov byte ptr [rax + rcx], 0 mov rdi, qword ptr [rsp + 16] cmp rdi, rbx je .LBB0_3 call operator delete(void*) .LBB0_3: xor eax, eax add rsp, 48 pop rbx ret mov rdi, rax call _Unwind_Resume .L.str: .asciz "ABCDEFGHIJKLMNOP"

Cuando se compila con el mismo -O3 . No entiendo por qué no reconoce que la a todavía no se usa, independientemente de que la cadena sea un byte más larga.

Esta pregunta es relevante para gcc 9.1 y clang 8.0, (en línea: https://gcc.godbolt.org/z/p1Z8Ns ) porque otros compiladores en mi observación descartan por completo la variable no utilizada (ellcc) o generan código para ella independientemente de la longitud de la cadena.


Me sorprendió que el compilador viera a través de un par std::string constructor / destructor hasta que vi su segundo ejemplo. No lo hizo Lo que está viendo aquí es la optimización de cadenas pequeñas y las optimizaciones correspondientes del compilador en torno a eso.

Las optimizaciones de cadenas pequeñas se producen cuando el propio objeto std::string es lo suficientemente grande como para contener el contenido de la cadena, un tamaño y posiblemente un bit de discriminación utilizado para indicar si la cadena está funcionando en modo de cadena pequeña o grande. En tal caso, no se producen asignaciones dinámicas y la cadena se almacena en el propio objeto std::string .

Los compiladores son realmente malos en eludir asignaciones y desasignaciones innecesarias, son tratados casi como si tuvieran efectos secundarios y, por lo tanto, son imposibles de eludir. Cuando supera el umbral de optimización de cadena pequeña, se producen asignaciones dinámicas y el resultado es lo que ve.

Como ejemplo

void foo() { delete new int; }

es el par de asignación / desasignación más simple, más tonto posible, sin embargo, gcc emite este conjunto incluso bajo O3

sub rsp, 8 mov edi, 4 call operator new(unsigned long) mov esi, 4 add rsp, 8 mov rdi, rax jmp operator delete(void*, unsigned long)