Bu işaretçiyi kullanmak, sıcak döngüde tuhaf deoptimizasyona neden olur


122

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 rightbir takip andsonra ve storeiçin targettampon. 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, loadher shift ( mov rdx,QWORD PTR [rdi]) ' den önce bellekten fazladan bir ek getirdik . Görünüşe göre targetgö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 targetburada 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-19ubuntu1ile -O3optimizasyon. Ben de clang++ 3.4-1ubuntu3benzer sonuçlarla denedim : Clang yöntemi yerel targetişaretçi ile vektörleştirebiliyor . Ancak, this->targetiş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.


5
Bunun neden olumsuz oy aldığından emin değilim - bu ilginç bir soru. FWIW Çözümün benzer olduğu işaretçi olmayan üye değişkenlerle benzer optimizasyon problemleri gördüm, yani üye değişkeni yöntemin ömrü boyunca yerel bir değişkende önbelleğe alın. Sanırım takma kurallarla ilgili bir şey mi?
Paul R

1
Görünüşe göre derleyici optimize etmiyor çünkü üyeye bazı "harici" kodlar aracılığıyla erişilmediğinden emin olamıyor. Yani üye dışarıda değiştirilebiliyorsa, her erişildiğinde yeniden yüklenmelidir. Görünüşe göre bir tür uçucu ...
Jean-Baptiste Yunès

Kullanmamak this->sadece sözdizimsel şekerdir. Sorun, değişkenlerin doğasıyla (yerel ve üye) ve derleyicinin bu olgudan çıkardığı şeylerle ilgilidir.
Jean-Baptiste Yunès

İşaretçi takma adlarıyla ilgisi var mı?
Yves Daoust

3
Daha anlamsal bir mesele olarak, "erken optimizasyon" yalnızca erken olan, yani profil oluşturmanın bir sorun olduğunu bulmadan önceki optimizasyona uygulanır. Bu durumda, özenle profil oluşturdunuz ve derlemesini çözdünüz ve bir sorunun kaynağını buldunuz ve bir çözüm formüle edip profilini çıkardınız. Bu çözümü uygulamak kesinlikle "erken" değildir.
raptortech97

Yanıtlar:


107

İşaretçi örtüşme, ironik bir şekilde thisve arasında sorun gibi görünüyor this->target. Derleyici, başlattığınız oldukça müstehcen olasılığı hesaba katıyor:

this->target = &this

Bu durumda, yazmak (ve dolayısıyla ) this->target[0]içeriğini değiştirir .thisthis->target

Bellek takma sorunu yukarıdakilerle sınırlı değildir. Prensip olarak, this->target[XX]uygun bir (in) değerinin herhangi bir kullanımı XXişaret eder this.

__restrict__Anahtar kelimeyle işaretçi değişkenlerini bildirerek bunun düzeltilebileceği C konusunda daha bilgiliyim .


18
Bunu teyit edebilirim! Şeklinden targetolarak uint8_tdeğiştirmek uint16_t(böylece katı örtüşme kuralları devreye girer) onu değiştirdi. İle uint16_tyük her zaman optimize edilir.
gexicide


3
İçeriğini değiştirmek thisdemek istediğiniz şey değildir (bu bir değişken değildir); içeriğini değiştirmeyi kastediyorsunuz *this.
Marc van Leeuwen

@gexicide zihin ne kadar katı takma adın devreye girip sorunu çözdüğünü açıklıyor?
HCSF

33

Katı örtüşme kuralları, char*herhangi bir başka işaretçiyi takma adlara izin verir . Yani this->targetbirlikte mayıs takma thisve kod yönteminde, Kodun ilk bölümü,

target[0] = t & 0x7;
target[1] = (t >> 3) & 0x7;
target[2] = (t >> 6) & 0x7;

Aslında

this->target[0] = t & 0x7;
this->target[1] = (t >> 3) & 0x7;
this->target[2] = (t >> 6) & 0x7;

olarak thisdeğiştirmek zaman değiştirilebilir this->targetiçeriği.

this->targetYerel bir değişkene önbelleğe alındıktan sonra , takma ad artık yerel değişkenle mümkün değildir.


1
Öyleyse, genel bir kural olarak söyleyebilir miyiz: Yapınızda bir char*veya void*yapınız olduğunda, yazmadan önce yerel bir değişkende önbelleğe aldığınızdan emin olun?
gexicide

5
Aslında char*, bir üye olarak gerekli değil, kullandığınız zamandır.
Jarod42

24

Buradaki sorun, bir char * aracılığıyla takma ad vermemize izin verildiğini ve böylelikle sizin durumunuzda derleyici optimizasyonunu engelleyen katı bir takma addır . Tanımsız davranış olacak farklı bir türden bir işaretçi ile takma ad vermemize izin verilmiyor, normalde SO'da uyumsuz işaretçi türleri aracılığıyla takma ad vermeye çalışan kullanıcıların bu sorunu görüyoruz. .

Bu uygulamaya mantıklıgörünecek uint8_t bir şekilde unsigned char ve biz bakarsak Coliru üzerinde cstdint içerdiği stdint.h typedefs uint8_t olarak aşağıdaki gibidir:

typedef unsigned char       uint8_t;

Char olmayan başka bir tür kullandıysanız, derleyici optimize edebilmelidir.

Bu, taslak C ++ standart bölümü 3.10 Lvalues ​​and rvalues bölümünde ele alınmıştır :

Bir program bir nesnenin depolanan değerine aşağıdaki türlerden biri dışındaki bir glvalue aracılığıyla erişmeye çalışırsa, davranış tanımsızdır

ve aşağıdaki madde işaretini içerir:

  • bir karakter veya imzasız karakter türü.

Not: uint8_t ≠ unsigned char ne zaman? Diye soran bir soruda olası geçici çözümlere ilişkin bir yorum yayınladım. ve öneri şuydu:

Önemsiz geçici çözüm, restrict anahtar sözcüğünü kullanmak veya işaretçiyi adresi hiçbir zaman alınmayan yerel bir değişkene kopyalamaktır, böylece derleyicinin uint8_t nesnelerinin takma adlarını değiştirip değiştiremeyeceği konusunda endişelenmesine gerek kalmaz.

C ++ restrict anahtar kelimesini desteklemediğinden, derleyici uzantısına güvenmeniz gerekir, örneğin gcc __restrict__ kullanır , bu yüzden bu tamamen taşınabilir değildir, ancak diğer öneri olmalıdır.


Bu, Standardın optimize ediciler için bir kural olandan daha kötü olduğu bir yerin, bir derleyicinin T tipindeki bir nesneye iki erişim arasında veya böyle bir erişim ile bir döngünün / işlevin başlangıcı veya sonunu varsaymasına izin vereceği bir yer örneğidir. burada meydana geldiğinde, depoya yapılan tüm erişimler, araya giren bir işlem bu nesneyi (veya bir işaretçi / başvuruyu) başka bir nesneye bir işaretçi veya referans türetmek için kullanmadıkça aynı nesneyi kullanacaktır . Böyle bir kural, bayt dizileriyle çalışan kodun performansını öldürebilecek "karakter türü istisnası" ihtiyacını ortadan kaldıracaktır.
supercat
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.