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 foo
uygulama 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 foo
ve 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 DoNotOptimize
buraya benzer standartlaştırma olasılığını araştırıyor .