Son zamanlarda garip bir deoptimizasyonla (veya daha doğrusu kaçırılmış optimizasyon fırsatıyla) karşılaştım.
3 bitlik tamsayılardan 8 bitlik tam sayılara kadar dizilerin verimli bir şekilde açılması için bu işlevi göz önünde bulundurun. Her döngü yinelemesinde 16 girişi paketler:
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;
}
}
İşte kodun bölümleri için oluşturulan derleme:
...
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
...
Oldukça verimli görünüyor. Bunun için, bir shift right
bir takip and
sonra ve store
için target
tampon. Ama şimdi, işlevi bir yapıdaki bir yönteme değiştirdiğimde ne olduğuna bakın:
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;
}
}
Oluşturulan montajın tamamen aynı olması gerektiğini düşündüm, ama değil. İşte bunun bir parçası:
...
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!
...
Gördüğünüz gibi, load
her shift ( mov rdx,QWORD PTR [rdi]
) ' den önce bellekten fazladan bir ek getirdik . Görünüşe göre target
göstericinin (artık yerel bir değişken yerine üye olan) içine kaydedilmeden önce her zaman yeniden yüklenmesi gerekiyor. Bu, kodu önemli ölçüde yavaşlatır (ölçümlerimde yaklaşık% 15).
İlk önce, C ++ bellek modelinin bir üye işaretçisinin bir kayıt defterinde saklanamayacağını, ancak yeniden yüklenmesi gerektiğini zorladığını düşündüm, ancak bu, pek çok uygulanabilir optimizasyonu imkansız hale getireceği için garip bir seçim gibi göründü. Bu yüzden derleyicinin target
burada bir kayıtta saklamamasına çok şaşırdım .
Üye işaretçisini yerel bir değişkene kendim önbelleğe almayı denedim:
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;
}
}
Bu kod ayrıca ek depolar olmadan "iyi" birleştiriciyi verir. Tahminime göre: Derleyicinin bir yapının üye göstericisinin yükünü kaldırmasına izin verilmez, bu nedenle böyle bir "sıcak işaretçi" her zaman yerel bir değişkende saklanmalıdır.
- Öyleyse, derleyici neden bu yükleri optimize edemiyor?
- Bunu yasaklayan C ++ bellek modeli mi? Yoksa sadece derleyicimin bir kusuru mu?
- Tahminim doğru mu veya optimizasyonun gerçekleştirilememesinin tam nedeni nedir?
Kullanılan derleyici oldu g++ 4.8.2-19ubuntu1
ile -O3
optimizasyon. Ben de clang++ 3.4-1ubuntu3
benzer sonuçlarla denedim : Clang yöntemi yerel target
işaretçi ile vektörleştirebiliyor . Ancak, this->target
işaretçiyi kullanmak aynı sonucu verir: Her depodan önce işaretçinin fazladan yüklenmesi.
Bazı benzer yöntemlerin montajcısını kontrol ettim ve sonuç aynı: Öyle görünüyor ki this
, böyle bir yük basitçe döngü dışında kaldırılabilse bile, üyelerinden birinin bir mağazadan önce her zaman yeniden yüklenmesi gerekiyor. Bu ek depolardan kurtulmak için çok sayıda kodu yeniden yazmak zorunda kalacağım, esas olarak işaretçiyi sıcak kodun üzerinde bildirilen yerel bir değişkene önbelleğe alarak. Ama her zaman, yerel bir değişkendeki bir işaretçiyi önbelleğe almak gibi ayrıntılarla uğraşmanın, derleyicilerin bu kadar zeki hale geldiği bu günlerde erken optimizasyona kesinlikle uygun olacağını düşündüm. Ama burada yanılmışım gibi görünüyor . Bir üye işaretçisini sıcak bir döngüde önbelleğe almak, gerekli bir manuel optimizasyon tekniği gibi görünüyor.
this->
sadece sözdizimsel şekerdir. Sorun, değişkenlerin doğasıyla (yerel ve üye) ve derleyicinin bu olgudan çıkardığı şeylerle ilgilidir.