Kompilator przestaje optymalizować nieużywany ciąg znaków podczas dodawania znaków
Jestem ciekaw dlaczego następujący fragment kodu:
#include <string>
int main()
{
std::string a = "ABCDEFGHIJKLMNO";
}
Po skompilowaniu z -O3
daje następujący kod:
main: # @main
xor eax, eax
ret
(doskonale rozumiem, że nie ma potrzeby używania nieużywanego a
, więc kompilator może całkowicie pominąć go z wygenerowanego kodu)
Jednak następujący program:
#include <string>
int main()
{
std::string a = "ABCDEFGHIJKLMNOP"; // <-- !!! One Extra P
}
Wydajność:
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"
W przypadku kompilacji z tym samym -O3
. Nie rozumiem, dlaczego nie rozpoznaje, że a
jest nadal nieużywany, niezależnie że ciąg jest o jeden bajt dłuższy.
To pytanie dotyczy gcc 9.1 i clang 8.0, (online: https://gcc.godbolt.org/z/p1Z8Ns ) ponieważ inne Kompilatory z mojej obserwacji albo całkowicie upuszczają nieużywaną zmienną (ellcc) albo generują dla niej kod niezależnie od długości łańcucha.
3 answers
Wynika to z małej optymalizacji ciągu. Gdy dane ciągu znaków są mniejsze lub równe 16 znakom, łącznie z terminatorem null, są przechowywane w buforze lokalnym do samego obiektu std::string
. W przeciwnym razie przydziela pamięć na stercie i przechowuje tam dane.
Pierwszy ciąg "ABCDEFGHIJKLMNO"
plus null terminator ma dokładnie rozmiar 16. Dodanie {[5] } powoduje przekroczenie bufora, stąd {[6] } jest wywoływane wewnętrznie, co nieuchronnie prowadzi do wywołania systemowego. Kompilator może zoptymalizuj coś, jeśli jest to możliwe, aby zapewnić, że nie ma skutków ubocznych. Wywołanie systemowe prawdopodobnie uniemożliwia zrobienie tego-przez constrast, zmiana bufora lokalnego na obiekt w budowie pozwala na taką analizę efektu ubocznego.
Śledzenie lokalnego bufora w libstdc++, Wersja 9.1, ujawnia te części 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; }; // ... };
, który pozwala dostrzec rozmiar lokalnego bufora _S_local_capacity
oraz sam lokalny bufor (_M_local_buf
). Gdy konstruktor uruchamia basic_string::_M_construct
wywołanie, masz w 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; }
Gdzie bufor lokalny jest wypełniony jego zawartością. Zaraz po tej części, dostajemy się do gałęzi, gdzie lokalna pojemność jest wyczerpana - przydzielana jest nowa pamięć masowa (poprzez alokację w M_create
), lokalny bufor jest kopiowany do nowej pamięci masowej i wypełniany resztą argumentu inicjalizującego:
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; }
Na marginesie, optymalizacja małych ciągów to całkiem sam temat. Aby poczuć, że jak Podkręcanie poszczególnych bitów może zrobić różnicę na dużą skalę, polecam tę rozmowę. Wspomina również, jak działa implementacja std::string
dostarczana z gcc
(libstdc++) i zmieniana w przeszłości, aby pasowała do nowszych wersji standardu.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-06-04 08:38:59
Byłem zaskoczony, że kompilator widział parę std::string
konstruktor/Destruktor, dopóki nie zobaczyłem twojego drugiego przykładu. To, co widzisz tutaj, to mała optymalizacja ciągów i odpowiednie optymalizacje z kompilatora wokół tego.
Optymalizacja małych łańcuchów ma miejsce wtedy, gdy obiekt std::string
jest wystarczająco duży, aby pomieścić zawartość łańcucha, rozmiar i ewentualnie bit rozróżniający używany do wskazania, czy łańcuch działa w trybie małym czy dużym łańcuchu. W takim case, nie występują dynamiczne przydziały, a łańcuch jest przechowywany w samym obiekcie std::string
.
Kompilatory są naprawdę złe w unikaniu niepotrzebnych przydziałów i dealokacji, są traktowane prawie tak, jakby miały skutki uboczne i dlatego są niemożliwe do uniknięcia. Gdy przekroczysz próg optymalizacji małych ciągów, pojawią się dynamiczne alokacje, a wynik jest tym, co widzisz.
Jako przykład
void foo() {
delete new int;
}
Jest najprostszą, najgłupszą możliwą parą alokacji/dealokacji, ale gcc emituje ten zespół nawet pod 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)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-06-03 10:43:27
Chociaż akceptowana odpowiedź jest prawidłowa, to od C++14 tak naprawdę jest tak, że wywołania new
i delete
mogą być zoptymalizowane. Zobacz to tajemne sformułowanie na cppreference:
Nowe-wyrażenia mogą znikać ... alokacje dokonywane za pomocą wymiennych funkcji alokacji. W przypadku usunięcia, pamięć może być dostarczona przez kompilator bez wywoływania funkcji alokacji (pozwala to również na optymalizację nieużywanego new-expression).
...
Zauważ, że ta optymalizacja jest dozwolona tylko wtedy, gdy new-expressions są używane, a nie Inne metody wywołania funkcji alokacji wymiennej:
delete[] new int[10];
można zoptymalizować, ale operator Nie mogę.
To pozwala kompilatorom całkowicie porzucić lokalny std::string
, nawet jeśli jest bardzo długi. W rzeczywistości-clang++ z libc++ robi to już (GodBolt), ponieważ libc++ używa wbudowanych __new
i __delete
w swojej implementacji std::string
- to "storage provided by the compiler". W ten sposób otrzymujemy:
main():
xor eax, eax
ret
Z nieużywanym ciągiem o dowolnej długości.
GCC nie działa, ale niedawno otworzyłem raporty o błędach na ten temat; zobacz to więc odpowiedz na linki.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-03-23 23:05:06