C ++ 'da ifade sırasını zorunlu kılma


111

Sabit bir sırayla yürütmek istediğim birkaç ifadem olduğunu varsayalım. Optimizasyon düzeyi 2 ile g ++ kullanmak istiyorum, böylece bazı ifadeler yeniden sıralanabilir. Belirli bir ifadeleri sıralamak için hangi araçlara ihtiyaç vardır?

Aşağıdaki örneği düşünün.

using Clock = std::chrono::high_resolution_clock;

auto t1 = Clock::now(); // Statement 1
foo();                  // Statement 2
auto t2 = Clock::now(); // Statement 3

auto elapsedTime = t2 - t1;

Bu örnekte 1-3 ifadelerinin verilen sırada yürütülmesi önemlidir. Ancak, derleyici düşünce ifadesi 2, 1 ve 3'ten bağımsız olup kodu aşağıdaki gibi çalıştıramaz mı?

using Clock=std::chrono::high_resolution_clock;

foo();                  // Statement 2
auto t1 = Clock::now(); // Statement 1
auto t2 = Clock::now(); // Statement 3

auto elapsedTime = t2 - t1;

34
Derleyici olmadıklarında bağımsız olduklarını düşünürse, derleyici bozulur ve daha iyi bir derleyici kullanmalısınız.
David Schwartz


1
olabilir __sync_synchronize()herhangi bir yardım?
vsz

3
@HowardHinnant: Standart C'nin anlamsal gücü, eğer böyle bir direktif tanımlanırsa ve diğer adlandırma kuralları, kendisinden önce yazılmış bir veri bariyerinden sonra yapılan okumaları muaf tutacak şekilde ayarlanırsa büyük ölçüde geliştirilebilir.
supercat

4
@DavidSchwartz Bu durumda mesele foo, farklı bir iş parçacığından gelen gözlemleri görmezden gelmesine izin verildiği gibi, derleyicinin yeniden sıralama sırasında yok saymasına izin verilen çalıştırma süresinin ölçülmesiyle ilgilidir .
CodesInChaos

Yanıtlar:


100

C ++ standartlar komitesiyle tartışıldıktan sonra biraz daha kapsamlı bir cevap vermeye çalışmak istiyorum. C ++ komitesinin bir üyesi olmanın yanı sıra, LLVM ve Clang derleyicilerinde de geliştiriciyim.

Temel olarak, bu dönüşümleri gerçekleştirmek için dizide bir bariyer veya bazı işlemler kullanmanın bir yolu yoktur. Temel sorun, tamsayı toplaması gibi bir şeyin işlemsel anlamının uygulama tarafından tamamen bilinmesidir . Onları simüle edebilir, doğru programlarla gözlemlenemeyeceğini bilir ve her zaman onları hareket ettirmekte özgürdür.

Bunu önlemeye çalışabilirdik, ancak son derece olumsuz sonuçları olur ve sonunda başarısız olur.

İlk olarak, derleyicide bunu önlemenin tek yolu, ona tüm bu temel işlemlerin gözlemlenebilir olduğunu söylemektir. Sorun şu ki, bu daha sonra derleyici optimizasyonlarının ezici çoğunluğunu engelleyecektir. Derleyicinin içinde, esasen zamanlamanın gözlemlenebilir olduğunu ancak başka hiçbir şeyin olmadığını modellemek için iyi bir mekanizmamız yok. Hangi işlemlerin zaman aldığı konusunda iyi bir modelimiz bile yok . Örnek olarak, 32 bitlik işaretsiz bir tamsayıyı 64 bitlik işaretsiz bir tam sayıya dönüştürmek zaman alır mı? X86-64'te sıfır zaman alır, ancak diğer mimarilerde sıfır olmayan süre alır. Burada genel olarak doğru bir cevap yok.

Ancak derleyicinin bu işlemleri yeniden düzenlemesini engellemede bazı kahramanlıklarla başarılı olsak bile, bunun yeterli olacağının garantisi yoktur. C ++ programınızı bir x86 makinesinde çalıştırmanın geçerli ve uyumlu bir yolunu düşünün: DynamoRIO. Programın makine kodunu dinamik olarak değerlendiren bir sistemdir. Yapabileceği bir şey, çevrimiçi optimizasyondur ve hatta zamanlamanın dışında tüm temel aritmetik komutları spekülatif olarak çalıştırabilir. Ve bu davranış, dinamik değerlendiricilere özgü değildir, gerçek x86 CPU da (çok daha az sayıda) talimatları speküle edecek ve bunları dinamik olarak yeniden sıralayacaktır.

Temel fark, aritmetiğin gözlemlenebilir olmadığı gerçeğidir (zamanlama düzeyinde bile), bilgisayarın katmanlarına nüfuz eden bir şeydir. Derleyici, çalışma zamanı ve çoğu zaman donanım için doğrudur. Gözlenebilir olmaya zorlamak, hem derleyiciyi önemli ölçüde kısıtlar, hem de donanımı önemli ölçüde kısıtlar.

Ancak tüm bunlar umudunuzu kaybetmenize neden olmamalıdır. Temel matematiksel işlemlerin yürütülmesini zamanlamak istediğinizde, güvenilir bir şekilde çalışan teknikleri iyi inceledik. Genellikle bunlar mikro kıyaslama yapılırken kullanılır . Bunun hakkında CppCon2015'te bir konuşma yaptım: https://youtu.be/nXaxk27zwlk

Orada gösterilen teknikler, Google'ın aşağıdaki gibi çeşitli mikro karşılaştırma kitaplıkları tarafından da sağlanmaktadır: https://github.com/google/benchmark#preventing-optimization

Bu tekniklerin anahtarı verilere odaklanmaktır. Hesaplamanın girdisini optimize edici için opak ve hesaplamanın sonucunu optimize edici için opak yaparsınız. Bunu yaptıktan sonra, güvenilir bir şekilde zamanlayabilirsiniz. Orijinal sorudaki örneğin gerçekçi bir versiyonuna bakalım, ancak foouygulama tarafından tamamen görülebilir tanımıyla . Ayrıca DoNotOptimize, burada bulabileceğiniz Google Benchmark kitaplığından (taşınabilir olmayan) bir sürümünü de çıkardım : https://github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h#L208

#include <chrono>

template <class T>
__attribute__((always_inline)) inline void DoNotOptimize(const T &value) {
  asm volatile("" : "+m"(const_cast<T &>(value)));
}

// The compiler has full knowledge of the implementation.
static int foo(int x) { return x * 2; }

auto time_foo() {
  using Clock = std::chrono::high_resolution_clock;

  auto input = 42;

  auto t1 = Clock::now();         // Statement 1
  DoNotOptimize(input);
  auto output = foo(input);       // Statement 2
  DoNotOptimize(output);
  auto t2 = Clock::now();         // Statement 3

  return t2 - t1;
}

Burada, girdi verilerinin ve çıktı verilerinin hesaplama etrafında optimize edilemez olarak işaretlenmesini foove yalnızca bu işaretçilerin etrafında hesaplanan zamanlamaların olmasını sağlıyoruz. Hesaplamayı kısaltmak için verileri kullandığınızdan, iki zamanlama arasında kalmanız garanti edilir ve yine de hesaplamanın kendisinin optimize edilmesine izin verilir. Yeni bir Clang / LLVM derlemesi tarafından oluşturulan sonuçta elde edilen x86-64 derlemesi şudur:

% ./bin/clang++ -std=c++14 -c -S -o - so.cpp -O3
        .text
        .file   "so.cpp"
        .globl  _Z8time_foov
        .p2align        4, 0x90
        .type   _Z8time_foov,@function
_Z8time_foov:                           # @_Z8time_foov
        .cfi_startproc
# BB#0:                                 # %entry
        pushq   %rbx
.Ltmp0:
        .cfi_def_cfa_offset 16
        subq    $16, %rsp
.Ltmp1:
        .cfi_def_cfa_offset 32
.Ltmp2:
        .cfi_offset %rbx, -16
        movl    $42, 8(%rsp)
        callq   _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, %rbx
        #APP
        #NO_APP
        movl    8(%rsp), %eax
        addl    %eax, %eax              # This is "foo"!
        movl    %eax, 12(%rsp)
        #APP
        #NO_APP
        callq   _ZNSt6chrono3_V212system_clock3nowEv
        subq    %rbx, %rax
        addq    $16, %rsp
        popq    %rbx
        retq
.Lfunc_end0:
        .size   _Z8time_foov, .Lfunc_end0-_Z8time_foov
        .cfi_endproc


        .ident  "clang version 3.9.0 (trunk 273389) (llvm/trunk 273380)"
        .section        ".note.GNU-stack","",@progbits

Burada, derleyicinin çağrıyı foo(input)tek bir talimata kadar optimize ettiğini addl %eax, %eax, ancak onu zamanlamanın dışına çıkarmadan veya sabit girdiye rağmen tamamen ortadan kaldırmadan görebilirsiniz.

Umarım bu yardımcı olur ve C ++ standartları komitesi, API'leri DoNotOptimizeburaya benzer standartlaştırma olasılığını araştırıyor .


1
Cevabınız için teşekkür ederim. Bunu yeni en iyi cevap olarak işaretledim. Bunu daha önce yapabilirdim, ancak bu yığın akışı sayfasını aylardır okumadım. C ++ programları yapmak için Clang derleyicisini kullanmakla çok ilgileniyorum. Diğer şeylerin yanı sıra, Clang'da değişken adlarında Unicode karakterleri kullanabilmeyi seviyorum. Stackoverflow'da Clang hakkında daha fazla soru soracağımı düşünüyorum.
S2108887

5
Bunun foo'nun tamamen optimize edilmesini nasıl engellediğini anlasam da, bunun neden çağrıların Clock::now()foo () 'ya göre yeniden sıralanmasını engellediğini biraz açıklayabilir misiniz ? Varsaymak zorundayız Optimizer mu DoNotOptimizeve Clock::now()erişebilir ve bunun sonucunda giriş ve çıkışına onları kravat olacak bazı ortak küresel durumunu değiştir olabilir? Veya optimize edicinin uygulamasının bazı mevcut sınırlamalarına mı güveniyorsunuz?
MikeMB

2
DoNotOptimizebu örnekte, sentetik olarak "gözlemlenebilir" bir olaydır. Sanki girişin temsili ile bazı terminallere kavramsal olarak görünür çıktı basmış gibidir. Saati okumak da gözlemlenebilir olduğu için (geçen zamanı gözlemliyorsunuz), programın gözlemlenebilir davranışını değiştirmeden yeniden sıralanamazlar.
Chandler Carruth

1
Hala "gözlemlenebilir" kavramından tam olarak emin değilim, eğer foofonksiyon bir süreliğine bloke olabilecek bir soketten okuma gibi bazı işlemler yapıyorsa, bu gözlemlenebilir bir işlemi sayar mı? Ve bu read"tamamen bilinen" bir işlem olmadığına göre (değil mi?), Kod sırasını koruyacak mı?
ravenisadesk

"Temel sorun, tamsayı toplama gibi bir şeyin işlemsel anlamının, uygulama tarafından tamamen bilinmesidir." Ama bana öyle geliyor ki, mesele tamsayı toplamanın anlamsallığı değil, foo () işlevini çağırmanın anlambilimidir. Foo () aynı derleme biriminde olmadığı sürece, foo () ve clock () 'un etkileşmediğini nasıl anlar?
Dave

59

Özet:

Yeniden sıralamayı engellemenin garantili bir yolu yok gibi görünüyor, ancak bağlantı zamanı / tam program optimizasyonu etkinleştirilmediği sürece , çağrılan işlevi ayrı bir derleme biriminde bulmak oldukça iyi bir bahis gibi görünüyor . (En azından GCC ile, mantık bunun diğer derleyiciler için de olası olduğunu öne sürse de.) Bu, işlev çağrısı maliyetiyle gelir - satır içi kod, tanımı gereği aynı derleme birimindedir ve yeniden sıralanmaya açıktır.

Orijinal cevap:

GCC, çağrıları -O2 optimizasyonu altında yeniden sıralar:

#include <chrono>
static int foo(int x)    // 'static' or not here doesn't affect ordering.
{
    return x*2;
}
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

GCC 5.3.0:

g++ -S --std=c++11 -O0 fred.cpp :

_ZL3fooi:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    %ecx, 16(%rbp)
        movl    16(%rbp), %eax
        addl    %eax, %eax
        popq    %rbp
        ret
_Z4fredi:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $64, %rsp
        movl    %ecx, 16(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -16(%rbp)
        movl    16(%rbp), %ecx
        call    _ZL3fooi
        movl    %eax, -4(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -32(%rbp)
        movl    -4(%rbp), %eax
        addq    $64, %rsp
        popq    %rbp
        ret

Fakat:

g++ -S --std=c++11 -O2 fred.cpp :

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        call    _ZNSt6chrono3_V212system_clock3nowEv
        leal    (%rbx,%rbx), %eax
        addq    $32, %rsp
        popq    %rbx
        ret

Şimdi, harici işlev olarak foo () ile:

#include <chrono>
int foo(int x);
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

g++ -S --std=c++11 -O2 fred.cpp :

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %ecx
        call    _Z3fooi
        movl    %eax, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %eax
        addq    $32, %rsp
        popq    %rbx
        ret

AMA, eğer bu -flto ile bağlantılıysa (bağlantı zamanı optimizasyonu):

0000000100401710 <main>:
   100401710:   53                      push   %rbx
   100401711:   48 83 ec 20             sub    $0x20,%rsp
   100401715:   89 cb                   mov    %ecx,%ebx
   100401717:   e8 e4 ff ff ff          callq  100401700 <__main>
   10040171c:   e8 bf f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401721:   e8 ba f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401726:   8d 04 1b                lea    (%rbx,%rbx,1),%eax
   100401729:   48 83 c4 20             add    $0x20,%rsp
   10040172d:   5b                      pop    %rbx
   10040172e:   c3                      retq

3
MSVC ve ICC de öyle. Clang, orijinal diziyi koruyan tek kişidir.
Cody Grey

3
t1 ve t2'yi hiçbir yerde kullanmıyorsunuz, bu yüzden sonucun atılabileceğini ve kodu yeniden sıralayabileceğini
düşünebilirsiniz

3
@Niall - Daha somut bir şey öneremem, ancak yorumumun altında yatan nedeni ima ettiğini düşünüyorum: Derleyici foo () 'nun şimdi () etkileyemeyeceğini biliyor, ya da tam tersi ve yeniden sıralama da öyle. Dış kapsam işlevlerini ve verilerini içeren çeşitli deneyler bunu doğruluyor gibi görünüyor. Bu, statik foo () 'nun bir dosya kapsamı değişkenine (N) bağlı olmasını içerir - N statik olarak bildirilirse, yeniden sıralama gerçekleşir, ancak statik olmayan olarak bildirilirse (yani diğer derleme birimleri tarafından görülebilir ve dolayısıyla potansiyel olarak now ()) yeniden sıralama gibi harici işlevler oluşmaz.
Jeremy

3
@ Lưu Vĩnh Phúc: Çağrıların kendilerinin göz ardı edilmemesi dışında. Bir kez daha, derleyici onların yan etkileri ne olabileceğini bilmediği için bu şüpheli - ama yok bu yan etkiler () foo davranışını etkilemek olamayacağını biliyoruz.
Jeremy

3
Ve son bir not: -flto (bağlantı zamanı optimizasyonu) belirtmek, başka türlü yeniden sıralanmamış durumlarda bile yeniden sıralamaya neden olur.
Jeremy

20

Yeniden sıralama, derleyici veya işlemci tarafından yapılabilir.

Çoğu derleyici, okuma-yazma talimatlarının yeniden sıralanmasını önlemek için platforma özgü bir yöntem sunar. Gcc'de bu

asm volatile("" ::: "memory");

( Daha fazla bilgi burada )

Bunun, okuma / yazma işlemlerine bağlı oldukları sürece yalnızca dolaylı olarak yeniden sıralama işlemlerini engellediğini unutmayın.

Uygulamada , sistem çağrısının Clock::now()böyle bir engelle aynı etkiye sahip olduğu bir sistem henüz görmedim . Emin olmak için ortaya çıkan montajı inceleyebilirsiniz.

Bununla birlikte, test edilen işlevin derleme sırasında değerlendirilmesi nadir değildir. "Gerçekçi" yürütmeyi sağlamak için foo(), giriş / çıkıştan veya bir volatileokumadan girdi türetmeniz gerekebilir .


Başka bir seçenek de satır içi yazmayı devre dışı bırakmak olabilir foo()- yine, bu derleyiciye özeldir ve genellikle taşınabilir değildir, ancak aynı etkiye sahip olacaktır.

Gcc'de bu, __attribute__ ((noinline))


@Ruslan temel bir konuyu gündeme getiriyor: Bu ölçüm ne kadar gerçekçi?

Yürütme süresi birçok faktörden etkilenir: Biri üzerinde çalıştığımız gerçek donanım, diğeri ise önbellek, bellek, disk ve CPU çekirdekleri gibi paylaşılan kaynaklara eşzamanlı erişimdir.

Dolayısıyla, karşılaştırılabilir zamanlamalar elde etmek için genellikle yaptığımız şey : düşük bir hata payı ile tekrarlanabilir olduklarından emin olun . Bu onları biraz yapay kılar.

"sıcak önbellek" ve "soğuk önbellek" yürütme performansı, büyüklük sırasına göre kolayca farklılık gösterebilir - ancak gerçekte, aralarında bir şey olacaktır ("ılık"?)


2
Your kesmek asmzamanlayıcı çağrılar arasında tabloların çalışma süresini etkiler: hafıza clobber sonra kod belleğindeki tüm değişkenleri yeniden zorundadır.
Ruslan

@Ruslan: Onların hackleri, benim değil. Farklı temizleme seviyeleri vardır ve böyle bir şey yapmak tekrarlanabilir sonuçlar için kaçınılmazdır.
peterchen

2
Asm ile hacklemenin yalnızca belleğe dokunan işlemler için bir engel oluşturmaya yardımcı olduğunu ve OP'nin bundan daha fazlasıyla ilgilendiğini unutmayın. Daha fazla ayrıntı için cevabıma bakın.
Chandler Carruth

11

C ++ dili, çeşitli şekillerde neyin gözlemlenebilir olduğunu tanımlar.

Eğer foo()hiçbir şey gözlemlenebilir yapar, o zaman tamamen ortadan kaldırılabilir. Eğer foo()"yerel" halde saklar değerleri (yığın halinde veya bir amacı, bir yerlerde olması) bir hesaplama yapar, ancak ve derleyici bir güvenli bir şekilde türetilmiş işaretçi almak kanıtlamak için Clock::now()kod, daha sonra hiçbir gözlenebilir sonuçları için orada Clock::now()aramaları taşımak .

Eğer foo()bir dosya veya ekran ve derleyici ile etkileşim olduğunu ispat edemez Clock::now()does not ardından bir dosya veya ekran etkileşim gözlemlenebilir davranış olduğu için, yapılamaz yeniden sıralama, dosya veya ekran etkileşim.

Kodu hareket etmemeye zorlamak için derleyiciye özgü korsanları kullanabilirsiniz (satır içi montaj gibi), başka bir yaklaşım da derleyicinizi zekice alt etmeye çalışmaktır.

Dinamik olarak yüklenmiş bir kitaplık oluşturun. Söz konusu koddan önce yükleyin.

Bu kütüphane bir şeyi açığa çıkarır:

namespace details {
  void execute( void(*)(void*), void *);
}

ve şu şekilde sarar:

template<class F>
void execute( F f ) {
  struct bundle_t {
    F f;
  } bundle = {std::forward<F>(f)};

  auto tmp_f = [](void* ptr)->void {
    auto* pb = static_cast<bundle_t*>(ptr);
    (pb->f)();
  };
  details::execute( tmp_f, &bundle );
}

Bu, boş bir lambda paketler ve dinamik kütüphaneyi derleyicinin anlayamayacağı bir bağlamda çalıştırmak için kullanır.

Dinamik kitaplığın içinde şunları yapıyoruz:

void details::execute( void(*f)(void*), void *p) {
  f(p);
}

ki bu oldukça basit.

Şimdi çağrıları yeniden sıralamak için execute, test kodunuzu derlerken anlayamayacağı dinamik kitaplığı anlamalıdır.

Yine de foo()sıfır yan etkiyle leri ortadan kaldırabilir , ancak biraz kazanırsınız, bazılarını kaybedersiniz.


19
"Başka bir yaklaşım, derleyicinizi zekice alt etmeye çalışmaktır" Bu ifade, tavşan deliğinden aşağı indiğinin bir işareti değilse, ne olduğunu bilmiyorum. :-)
Cody Grey

1
Bir kod bloğunun yürütülmesi için gereken sürenin, derleyicilerin sürdürmesi gereken "gözlemlenebilir" bir davranış olarak görülmediğini belirtmenin faydalı olabileceğini düşünüyorum . Bir kod bloğunu yürütme zamanı "gözlemlenebilir" ise, o zaman performans optimizasyonunun hiçbir şekline izin verilmez. C ve C ++ için, bir derleyicinin engelden önceki tüm yan etkiler üretilen kod [kod olan verilerin tam olarak ...
supercat

1
... donanım önbellekleri aracılığıyla yayılan, bunu yapmak için donanıma özgü araçlar kullanması gerekir, ancak donanıma özgü tüm yazmalar tamamlanana kadar beklemenin bir yolu, tüm bekleyen yazmaların derleyici tarafından izlenmesini sağlamak için bir engel yönergesi olmadan işe yaramaz. Donanıma gönderilen tüm yazma işlemlerinin tamamlandığından emin olması istenmeden önce donanıma gönderilmelidir.] Sahte volatileerişim veya dış koda çağrı kullanmadan bunu her iki dilde de yapmanın hiçbir yolunu bilmiyorum .
supercat

4

Hayır yapamaz. C ++ standardına göre [intro.execution]:

14 Bir tam ifade ile ilişkili her değer hesaplaması ve yan etki, değerlendirilecek bir sonraki tam ifade ile ilişkili her değer hesaplamasından ve yan etkiden önce sıralanır.

Tam ifade, temelde noktalı virgülle sonlandırılan bir ifadedir. Gördüğünüz gibi yukarıdaki kural, ifadelerin sırayla yürütülmesi gerektiğini şart koşmaktadır. Öyle içindeki derleyici daha başıboş izin verildiğini ifadeleri (yani o dışındaki siparişlerde bir açıklama oluşturan ifadeleri değerlendirmek için izin bazı koşulda olduğu soldan sağa veya başka bir şey spesifik).

Varsa kuralının uygulanacağı koşulların burada karşılanmadığına dikkat edin. Herhangi bir derleyicinin , sistem saatini almak için çağrıları yeniden sıralamanın gözlemlenebilir program davranışını etkilemeyeceğini kanıtlayabileceğini düşünmek mantıksızdır . Zamanı almak için iki çağrının, gözlemlenen davranışı değiştirmeden yeniden düzenlenebileceği bir durum olsaydı, bunu kesin bir şekilde çıkarabilmek için yeterli anlayışa sahip bir programı analiz eden bir derleyici üretmek son derece verimsiz olurdu.


12
Yine de sanki kuralı var
MM

18
By olarak -eğer kural o gözlemlenebilir davranışı değiştirmez olarak derleyici sürece koduna şeyi yapabilir. Yürütme zamanı gözlemlenemez. Böylece, sonuç aynı olduğu sürece rastgele kod satırlarını yeniden sıralayabilir (çoğu derleyici mantıklı bir şey yapar ve zaman çağrılarını yeniden sıralamaz, ancak bu gerekli değildir)
Revolver_Ocelot

6
Yürütme zamanı gözlemlenemez. Bu oldukça tuhaf. Pratik, teknik olmayan bir bakış açısından, uygulama zamanı (diğer adıyla "performans") çok gözlemlenebilir.
Frédéric Hamidi

3
Zamanı nasıl ölçtüğünüze bağlı. Standart C ++ 'da bir kod gövdesini çalıştırmak için alınan saat döngülerinin sayısını ölçmek mümkün değildir.
Peter

3
@dba Birkaç şeyi karıştırıyorsunuz. Bağlayıcı artık Win16 uygulamaları oluşturamaz, bu yeterince doğru, ancak bunun nedeni bu tür bir ikili dosya oluşturmak için desteği kaldırmış olmalarıdır. WIn16 uygulamaları PE biçimini kullanmaz. Bu, derleyicinin veya bağlayıcının API işlevleri hakkında özel bilgiye sahip olduğu anlamına gelmez. Diğer sorun, çalışma zamanı kitaplığıyla ilgilidir. NT 4 üzerinde çalışan bir ikili dosya oluşturmak için MSVC'nin en son sürümünü edinmek kesinlikle sorun değil. Bunu yaptım. Sorun, mevcut olmayan işlevleri çağıran CRT'ye bağlanmaya çalıştığınız anda ortaya çıkar.
Cody Grey

2

Hayır.

Bazen "sanki" kuralı gereği ifadeler yeniden sıralanabilir. Bunun nedeni, mantıksal olarak birbirlerinden bağımsız olmaları değil, bu bağımsızlığın, programın anlamını değiştirmeden böyle bir yeniden sıralamanın gerçekleşmesine izin vermesidir.

Şimdiki zamanı alan bir sistem çağrısının taşınması, bu koşulu açıkça karşılamaz. Bilerek veya bilmeyerek bunu yapan bir derleyici uyumsuzdur ve gerçekten saçmadır.

Genel olarak, bir sistem çağrısı ile sonuçlanan herhangi bir ifadenin agresif bir şekilde optimize eden bir derleyici tarafından bile "ikinci tahmin" yapılmasını beklemem. Sistem çağrısının ne yaptığını yeterince bilmiyor.


5
Aptalca olacağını kabul ediyorum, ama buna uygunsuz demeyeceğim . Derleyici, somut sistemde sistem çağrısının tam olarak ne yaptığı ve yan etkileri olup olmadığı konusunda bilgi sahibi olabilir. Derleyicilerin, standart bunu yasakladığı için değil, daha iyi kullanıcı deneyimine izin vererek, genel kullanım durumunu kapsayacak şekilde bu tür çağrıyı yeniden düzenlememelerini beklerdim.
Revolver_Ocelot

4
@Revolver_Ocelot: Programın anlamını değiştiren optimizasyonlar (tamam, kopya seçimi için kaydetme), kabul etseniz de etmeseniz de standarda uymuyor.
Orbit'te Hafiflik Yarışları

6
Önemsiz durumu int x = 0; clock(); x = y*2; clock();vardır hayır için tanımlanmış yolları clock()durumuna ile etkileşim kod x. C ++ standardına göre, ne yaptığını bilmek zorunda değildir clock()- yığını inceleyebilir (ve hesaplama gerçekleştiğinde fark edebilir), ancak bu C ++ 'nın sorunu değildir .
Yakk - Adam Nevraumont

5
Yakk'ın noktasını daha da ileri götürmek gerekirse: Sistem çağrılarının yeniden sıralanması doğrudur, böylece ilkinin sonucu atanır t2ve ikincisi atanırsa t1, bu değerler kullanılırsa uyumsuz ve aptalca olur, bu cevabın kaçırdığı şey şudur: uygun bir derleyici bazen bir sistem çağrısı boyunca başka bir kodu yeniden sıralayabilir. Bu durumda, neyin işe yaradığını bilmesi koşuluyla foo()(örneğin, onu satır içine aldığı için) ve dolayısıyla (gevşek bir şekilde konuşursak) saf bir işlev olması koşuluyla, onu hareket ettirebilir.
Steve Jessop

1
.. yine gevşek bir şekilde konuşursak, bunun nedeni gerçek uygulamanın (soyut makine olmasa da) y*ysistem çağrısından önce spekülatif olarak hesaplamayacağının garantisinin olmamasıdır , sadece eğlence için. Ayrıca, gerçek uygulamanın bu spekülatif hesaplamanın sonucunu daha sonra, hangi noktada xkullanılırsa kullanılsın kullanmayacağının, dolayısıyla çağrıların arasında hiçbir şey yapmayacağının garantisi de yoktur clock(). Aynısı foo, satır içi bir işlevin yaptığı her şey için de geçerlidir, ancak yan etkileri yoktur ve değiştirilebilecek duruma bağlı olamaz clock().
Steve Jessop

0

noinline işlev + satır içi montaj kara kutusu + tam veri bağımlılıkları

Bu, https://stackoverflow.com/a/38025837/895245'e dayanmaktadır, ancak neden ::now()orada yeniden sıralanamayacağına dair net bir gerekçe görmediğim için paranoyak olmayı ve onu bir noinline işlevinin içine koymayı asm.

Bu şekilde , veri bağımlılığını noinline"bağladığından" yeniden sıralama yapılamayacağından oldukça eminim ::now.

main.cpp

#include <chrono>
#include <iostream>
#include <string>

// noinline ensures that the ::now() cannot be split from the __asm__
template <class T>
__attribute__((noinline)) auto get_clock(T& value) {
    // Make the compiler think we actually use / modify the value.
    // It can't "see" what is going on inside the assembly string.
    __asm__ __volatile__ ("" : "+g" (value));
    return std::chrono::high_resolution_clock::now();
}

template <class T>
static T foo(T niters) {
    T result = 42;
    for (T i = 0; i < niters; ++i) {
        result = (result * result) - (3 * result) + 1;
    }
    return result;
}

int main(int argc, char **argv) {
    unsigned long long input;
    if (argc > 1) {
        input = std::stoull(argv[1], NULL, 0);
    } else {
        input = 1;
    }

    // Must come before because it could modify input
    // which is passed as a reference.
    auto t1 = get_clock(input);
    auto output = foo(input);
    // Must come after as it could use the output.
    auto t2 = get_clock(output);
    std::cout << "output " << output << std::endl;
    std::cout << "time (ns) "
              << std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count()
              << std::endl;
}

GitHub yukarı akış .

Derleyin ve çalıştırın:

g++ -ggdb3 -O3 -std=c++14 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out 1000
./main.out 10000
./main.out 100000

Bu yöntemin tek küçük dezavantajı, callqbir inlineyönteme fazladan bir talimat eklememizdir . şunları içeren objdump -CDgösterir main:

    11b5:       e8 26 03 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>
    11ba:       48 8b 34 24             mov    (%rsp),%rsi
    11be:       48 89 c5                mov    %rax,%rbp
    11c1:       b8 2a 00 00 00          mov    $0x2a,%eax
    11c6:       48 85 f6                test   %rsi,%rsi
    11c9:       74 1a                   je     11e5 <main+0x65>
    11cb:       31 d2                   xor    %edx,%edx
    11cd:       0f 1f 00                nopl   (%rax)
    11d0:       48 8d 48 fd             lea    -0x3(%rax),%rcx
    11d4:       48 83 c2 01             add    $0x1,%rdx
    11d8:       48 0f af c1             imul   %rcx,%rax
    11dc:       48 83 c0 01             add    $0x1,%rax
    11e0:       48 39 d6                cmp    %rdx,%rsi
    11e3:       75 eb                   jne    11d0 <main+0x50>
    11e5:       48 89 df                mov    %rbx,%rdi
    11e8:       48 89 44 24 08          mov    %rax,0x8(%rsp)
    11ed:       e8 ee 02 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>

bu yüzden bunun foosatır içi olduğunu, ancak get_clockolmadığını ve çevrelediğini görüyoruz .

get_clock Ancak kendisi, yığına bile dokunmayan tek bir yaprak çağrısı optimize edilmiş talimattan oluşan son derece verimlidir:

00000000000014e0 <auto get_clock<unsigned long long>(unsigned long long&)>:
    14e0:       e9 5b fb ff ff          jmpq   1040 <std::chrono::_V2::system_clock::now()@plt>

Saat hassasiyetinin kendisi sınırlı olduğundan, bir fazladanın zamanlama etkilerini fark etme olasılığınızın düşük olduğunu düşünüyorum jmpq. Paylaşılan bir kitaplıkta callolduğundan bağımsız olarak birinin gerekli olduğuna dikkat edin ::now().

::now()Veri bağımlılığı ile satır içi derlemeden çağrı

Bu, jmpqyukarıda bahsedilen fazlalıkların bile üstesinden gelerek, mümkün olan en verimli çözüm olacaktır .

Bu maalesef aşağıdaki adreste gösterildiği gibi doğru şekilde yapmak son derece zordur: Genişletilmiş satır içi ASM'de printf çağrısı

Ancak, zaman ölçümünüz bir çağrı olmaksızın doğrudan hat içi montajda yapılabiliyorsa, bu teknik kullanılabilir. Bu durum, örneğin gem5 sihirli enstrümantasyon talimatları , x86 RDTSC (artık bunun temsilci olup olmadığından emin değil) ve muhtemelen diğer performans sayaçları için geçerlidir.

İlgili konular:

GCC 8.3.0, Ubuntu 19.04 ile test edilmiştir.


1
Normalde bir dökülmeyi / yeniden yüklemeyi zorlamanız gerekmez "+m", kullanmak "+r"derleyicinin bir değeri gerçekleştirmesini sağlamak için çok daha etkili bir yoldur ve sonra değişkenin değiştiğini varsayar.
Peter Cordes
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.