Użycie tego wskaźnika powoduje dziwną deoptymizację w hot loop
Ostatnio natknąłem się na dziwną deoptymizację (a raczej przegapiłem możliwość optymalizacji).
Rozważmy tę funkcję do efektywnego rozpakowywania tablic z 3-bitowych liczb całkowitych do 8-bitowych liczb całkowitych. Rozpakowuje 16 Int w każdej iteracji pętli:
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;
}
}
Oto wygenerowany zestaw dla części kodu:
...
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
...
Wygląda całkiem sprawnie. Po prostu shift right
, a następnie and
, a następnie store
do bufora target
. Ale teraz, zobacz co się dzieje, gdy zmienię funkcja do metody w strukturze:
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;
}
}
Myślałem, że wygenerowany assembly powinien być taki sam, ale tak nie jest. Oto jego część:
...
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!
...
Jak widzicie, przed każdą zmianą wprowadziliśmy dodatkowy redundantny load
z pamięci (mov rdx,QWORD PTR [rdi]
). Wygląda na to, że wskaźnik target
(który jest teraz członkiem zamiast zmiennej lokalnej) musi być zawsze przeładowany przed zapisaniem do niego. to znacznie spowalnia kod (OKOŁO 15% W moim pomiary).
Najpierw pomyślałem, że może Model pamięci C++ wymusza, że wskaźnik członka może nie być przechowywany w rejestrze, ale musi być przeładowany, ale wydawało się to niezręcznym wyborem, ponieważ uniemożliwiłoby to wiele opłacalnych optymalizacji. Byłem więc bardzo zaskoczony, że kompilator nie przechowuje target
w rejestrze tutaj.
Sam próbowałem buforować wskaźnik member do zmiennej lokalnej:
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;
}
}
Ten kod daje również" dobry " asembler bez dodatkowe sklepy. Domyślam się, że kompilator nie może podnosić obciążenia wskaźnika elementu struktury, więc taki "gorący wskaźnik" powinien być zawsze przechowywany w zmiennej lokalnej.
- dlaczego więc kompilator nie jest w stanie zoptymalizować tych obciążeń?
- czy to model pamięci C++ tego zabrania? Czy jest to po prostu wada mojego kompilatora?
- Czy moje przypuszczenie jest poprawne lub jaki jest dokładny powód, dla którego optymalizacja nie może być wystąpili
Używany kompilator był g++ 4.8.2-19ubuntu1
z -O3
optymalizacją. Próbowałem również clang++ 3.4-1ubuntu3
z podobnymi wynikami: Clang jest nawet w stanie wektoryzować metodę za pomocą lokalnego wskaźnika target
. Jednak użycie wskaźnika this->target
daje ten sam wynik: dodatkowe obciążenie wskaźnika przed każdym sklepem.
Sprawdziłem w asemblerze podobne metody i wynik jest taki sam: wygląda na to, że element this
zawsze musi być przeładowany przed sklepem, nawet jeśli takie obciążenie można go po prostu podnieść poza pętlę. Będę musiał przepisać dużo kodu, aby pozbyć się tych dodatkowych sklepów, głównie przez buforowanie wskaźnika do lokalnej zmiennej, która jest zadeklarowana powyżej gorącego kodu. ale zawsze myślałem, że manipulowanie takimi szczegółami jak buforowanie wskaźnika w lokalnej zmiennej z pewnością kwalifikuje się do przedwczesnej optymalizacji w tych dniach, gdzie Kompilatory stały się tak sprytne. Ale wygląda na to, że się mylę . Buforowanie wskaźnika członka w gorącej pętli wydaje się być niezbędna ręczna technika optymalizacji.
3 answers
Pointer aliasing wydaje się być problemem, jak na ironię między this
a this->target
. Kompilator bierze pod uwagę raczej nieprzyzwoitą możliwość, którą zainicjowałeś:
this->target = &this
W takim przypadku zapisanie do this->target[0]
zmieniłoby zawartość this
(a więc ten - > target).
Problem aliasingu pamięci nie ogranicza się do powyższego. W zasadzie każde użycie this->target[XX]
z odpowiednią wartością XX
może wskazywać na this
.
Jestem lepiej zorientowany w C, gdzie można temu zaradzić, deklarując zmienne wskaźnika za pomocą słowa kluczowego__ restrict__.
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
2014-10-10 16:35:36
Ścisłe reguły aliasingu pozwalają char*
aliasować dowolny inny wskaźnik. Więc this->target
może być aliasem z this
, a w metodzie kodu pierwsza część kodu,
target[0] = t & 0x7;
target[1] = (t >> 3) & 0x7;
target[2] = (t >> 6) & 0x7;
Jest w rzeczywistości
this->target[0] = t & 0x7;
this->target[1] = (t >> 3) & 0x7;
this->target[2] = (t >> 6) & 0x7;
Jako {[4] } mogą być modyfikowane przy modyfikowaniu this->target
treści.
Po zapisaniu this->target
w pamięci podręcznej do zmiennej lokalnej, alias nie jest już możliwy dla zmiennej lokalnej.
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
2014-10-10 16:36:27
Problemem jest tutaj ścisłe aliasowanie , które mówi, że możemy aliasować przez znak * , co zapobiega optymalizacji kompilatora w Twoim przypadku. Nie wolno nam aliasować za pomocą wskaźnika innego typu, który byłby niezdefiniowanym zachowaniem, Zwykle włączonym, więc widzimy ten problem, który polega na tym, że użytkownicy próbują aliasu za pomocą niekompatybilnych typów wskaźników .
Wydaje się rozsądne zaimplementowanie uint8_t jako unsigned char i jeśli spójrz na cstdint na Coliru zawiera stdint.h który wpisał uint8_t następująco:
typedef unsigned char uint8_t;
Jeśli użyłeś innego typu non-char, kompilator powinien być w stanie zoptymalizować.
Jest to omówione w sekcji draft C++ standard3.10
Lvalues i rvalues , który mówi:
Jeśli program próbuje uzyskać dostęp do zapisanej wartości obiektu za pomocą wartości glvalue innej niż jedna z następujące typy zachowania to undefined
I zawiera następujący punkt:
- Typ char lub unsigned char.
Uwaga, zamieściłem komentarz na temat możliwej pracy wokółw pytaniu, które pyta Kiedy jest uint8_t ≠ unsigned char? A zalecenie brzmiało:
Banalnym obejściem jest jednak użycie słowa kluczowego ogranicz lub skopiuj wskaźnik do zmiennej lokalnej, której adres nigdy nie jest tak że kompilator nie musisz się martwić, czy uint8_t obiekty mogą go nazywać aliasem.
Ponieważ C++ nie obsługuje słowa kluczowego restrict , musisz polegać na rozszerzeniu kompilatora, na przykład gcc używa _ _ restrict _ _ więc nie jest to całkowicie przenośne, ale inna sugestia powinna być.
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
2017-05-23 12:01:28