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)