Saya ingin mencoba memberikan jawaban yang lebih komprehensif setelah ini didiskusikan dengan komite standar C ++. Selain menjadi anggota komite C ++, saya juga seorang pengembang di LLVM dan kompiler Clang.
Pada dasarnya, tidak ada cara untuk menggunakan penghalang atau beberapa operasi dalam urutan untuk mencapai transformasi ini. Masalah mendasar adalah bahwa semantik operasional dari sesuatu seperti penjumlahan integer benar - benar diketahui implementasinya. Ia dapat mensimulasikan mereka, mengetahui bahwa mereka tidak dapat diamati oleh program yang benar, dan selalu bebas untuk memindahkannya.
Kami dapat mencoba mencegah ini, tetapi hasilnya akan sangat negatif dan pada akhirnya akan gagal.
Pertama, satu-satunya cara untuk mencegah hal ini pada compiler adalah dengan memberitahukan bahwa semua operasi dasar ini dapat diamati. Masalahnya adalah hal ini kemudian akan menghalangi sebagian besar pengoptimalan compiler. Di dalam kompiler, pada dasarnya kita tidak memiliki mekanisme yang baik untuk memodelkan bahwa pengaturan waktunya dapat diamati tetapi tidak ada yang lain. Kami bahkan tidak memiliki model yang baik tentang operasi apa yang membutuhkan waktu . Sebagai contoh, apakah mengonversi integer 32-bit unsigned menjadi integer 64-bit unsigned membutuhkan waktu? Dibutuhkan waktu nol pada x86-64, tetapi pada arsitektur lain dibutuhkan waktu bukan nol. Tidak ada jawaban yang benar secara umum di sini.
Tetapi bahkan jika kami berhasil melewati beberapa langkah heroik dalam mencegah compiler menyusun ulang operasi ini, tidak ada jaminan ini akan cukup. Pertimbangkan cara yang valid dan sesuai untuk menjalankan program C ++ Anda pada mesin x86: DynamoRIO. Ini adalah sistem yang secara dinamis mengevaluasi kode mesin program. Satu hal yang dapat dilakukannya adalah pengoptimalan online, dan bahkan mampu secara spekulatif mengeksekusi seluruh rangkaian instruksi aritmatika dasar di luar waktu. Dan perilaku ini tidak unik untuk evaluator dinamis, CPU x86 yang sebenarnya juga akan berspekulasi (jumlah yang jauh lebih kecil) instruksi dan menyusunnya kembali secara dinamis.
Realisasi esensial adalah fakta bahwa aritmatika tidak dapat diamati (bahkan pada tingkat waktu) adalah sesuatu yang menembus lapisan komputer. Hal ini berlaku untuk kompiler, runtime, dan seringkali bahkan untuk perangkat keras. Memaksanya agar dapat diamati akan secara dramatis membatasi kompiler, tetapi juga akan secara dramatis membatasi perangkat keras.
Tetapi semua ini seharusnya tidak membuat Anda kehilangan harapan. Jika Anda ingin mengatur waktu pelaksanaan operasi matematika dasar, kami telah mempelajari teknik yang bekerja dengan andal. Biasanya ini digunakan saat melakukan pembandingan mikro . Saya memberikan ceramah tentang ini di CppCon2015: https://youtu.be/nXaxk27zwlk
Teknik yang ditampilkan di sana juga disediakan oleh berbagai pustaka patokan mikro seperti Google: https://github.com/google/benchmark#preventing-optimization
Kunci dari teknik ini adalah fokus pada data. Anda membuat masukan ke penghitungan buram ke pengoptimal dan hasil penghitungan buram ke pengoptimal. Setelah Anda selesai melakukannya, Anda dapat mengatur waktunya dengan andal. Mari kita lihat versi realistik dari contoh dalam pertanyaan awal, tetapi dengan definisi foo
terlihat sepenuhnya untuk implementasi. Saya juga telah mengekstrak versi (non-portabel) dari DoNotOptimize
pustaka Google Benchmark yang dapat Anda temukan di sini: 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;
}
Di sini kami memastikan bahwa data masukan dan data keluaran ditandai sebagai tidak dapat dioptimalkan selama penghitungan foo
, dan hanya di sekitar penanda tersebut pengaturan waktu dihitung. Karena Anda menggunakan data untuk menjepit penghitungan, dijamin untuk tetap berada di antara dua pengaturan waktu, namun penghitungan itu sendiri diizinkan untuk dioptimalkan. Rakitan x86-64 yang dihasilkan yang dihasilkan oleh build Clang / LLVM terbaru adalah:
% ./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
Di sini Anda dapat melihat compiler mengoptimalkan panggilan ke foo(input)
satu instruksi addl %eax, %eax
, tetapi tanpa memindahkannya ke luar timing atau menghilangkannya sepenuhnya meskipun input konstan.
Semoga ini bisa membantu, dan komite standar C ++ sedang melihat kemungkinan standarisasi API yang mirip dengan di DoNotOptimize
sini.