Menerapkan urutan pernyataan di C ++

111

Misalkan saya memiliki sejumlah pernyataan yang ingin saya jalankan dalam urutan tetap. Saya ingin menggunakan g ++ dengan pengoptimalan level 2, sehingga beberapa pernyataan dapat diatur ulang. Alat apa yang dimiliki seseorang untuk menegakkan urutan pernyataan tertentu?

Perhatikan contoh berikut.

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;

Dalam contoh ini, penting bahwa pernyataan 1-3 dieksekusi dalam urutan tertentu. Namun, tidak dapatkah kompilator menganggap pernyataan 2 tidak bergantung pada 1 dan 3 dan menjalankan kode sebagai berikut?

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;
S2108887
sumber
34
Jika kompilator menganggapnya independen padahal sebenarnya tidak, kompilator rusak dan Anda harus menggunakan kompilator yang lebih baik.
David Schwartz
1
bisa __sync_synchronize()membantu?
vsz
3
@ HowardHinnant: Kekuatan semantik standar C akan meningkat pesat jika arahan seperti itu didefinisikan, dan jika aturan aliasing disesuaikan untuk mengecualikan pembacaan yang dilakukan setelah pembatas data yang ditulis sebelumnya.
supercat
4
@DavidSchwartz Dalam hal ini, ini tentang mengukur waktu yang foodibutuhkan untuk menjalankan, yang boleh diabaikan oleh kompilator saat menyusun ulang, seperti halnya diizinkan untuk mengabaikan pengamatan dari utas yang berbeda.
CodesInChaos

Jawaban:

100

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 footerlihat sepenuhnya untuk implementasi. Saya juga telah mengekstrak versi (non-portabel) dari DoNotOptimizepustaka 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 DoNotOptimizesini.

Chandler Carruth
sumber
1
Terima kasih atas jawaban Anda. Saya telah menandainya sebagai jawaban terbaik baru. Saya bisa melakukan ini sebelumnya, tetapi saya belum membaca halaman stackoverflow ini selama berbulan-bulan. Saya sangat tertarik menggunakan kompiler Clang untuk membuat program C ++. Antara lain, saya suka bahwa seseorang dapat menggunakan karakter Unicode dalam nama variabel di Clang. Saya rasa saya akan menanyakan lebih banyak pertanyaan tentang Clang di Stackoverflow.
S2108887
5
Meskipun saya memahami bagaimana hal ini mencegah foo dioptimalkan sepenuhnya, dapatkah Anda menjelaskan sedikit mengapa hal ini mencegah panggilan untuk Clock::now()diurutkan ulang relatif terhadap foo ()? Apakah pengoptimal harus berasumsi bahwa DoNotOptimizedan Clock::now()memiliki akses ke dan mungkin mengubah beberapa keadaan global umum yang pada gilirannya akan mengikat mereka ke dalam dan keluaran? Atau apakah Anda mengandalkan beberapa batasan penerapan pengoptimal saat ini?
MikeMB
2
DoNotOptimizedalam contoh ini adalah peristiwa sintetik yang "dapat diamati". Seolah-olah itu secara nosional mencetak output yang terlihat ke beberapa terminal dengan representasi input. Sejak membaca jam juga dapat diamati (Anda mengamati berlalunya waktu) mereka tidak dapat diatur ulang tanpa mengubah perilaku program yang dapat diamati.
Chandler Carruth
1
Saya masih kurang jelas dengan konsep "observable", jika foofungsinya melakukan beberapa operasi seperti membaca dari soket yang mungkin diblokir untuk sementara waktu, apakah ini termasuk operasi yang dapat diamati? Dan karena readini bukan operasi yang "benar-benar diketahui" (bukan?), Apakah kode akan tetap teratur?
ravenisadesk
"Masalah mendasar adalah bahwa semantik operasional dari sesuatu seperti penjumlahan integer sepenuhnya diketahui implementasinya." Tapi menurut saya masalahnya bukanlah semantik penambahan integer, itu semantik memanggil fungsi foo (). Kecuali foo () berada dalam unit kompilasi yang sama, bagaimana ia tahu bahwa foo () dan clock () tidak berinteraksi?
Dave
59

Ringkasan:

Tampaknya tidak ada cara yang dijamin untuk mencegah pengubahan urutan, tetapi selama pengoptimalan waktu tautan / program penuh tidak diaktifkan, menempatkan fungsi yang dipanggil di unit kompilasi terpisah tampaknya merupakan taruhan yang cukup bagus . (Setidaknya dengan GCC, meskipun logika akan menyarankan bahwa hal ini mungkin terjadi pada kompiler lain juga.) Ini datang dengan biaya dari pemanggilan fungsi - kode inline menurut definisi dalam unit kompilasi yang sama dan terbuka untuk penyusunan ulang.

Jawaban asli:

GCC menyusun ulang panggilan di bawah -O2 optimization:

#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

Tapi:

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

Sekarang, dengan foo () sebagai fungsi eksternal:

#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

TAPI, jika ini ditautkan dengan -flto (pengoptimalan waktu tautan):

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
Jeremy
sumber
3
Begitu juga dengan MSVC dan ICC. Dentang adalah satu-satunya yang tampaknya mempertahankan urutan aslinya.
Cody Gray
3
Anda tidak menggunakan t1 dan t2 di mana pun sehingga mungkin mengira hasilnya dapat dibuang dan menyusun ulang kode
phuclv
3
@Niall - Saya tidak dapat menawarkan apa pun yang lebih konkret, tetapi menurut saya komentar saya mengacu pada alasan yang mendasarinya: Kompilator tahu bahwa foo () tidak dapat memengaruhi now (), atau sebaliknya, dan begitu juga dengan penyusunan ulang. Berbagai eksperimen yang melibatkan fungsi dan data lingkup eksternal tampaknya mengkonfirmasi hal ini. Ini termasuk memiliki foo () statis yang bergantung pada variabel lingkup file N - jika N dideklarasikan sebagai statis, pengubahan urutan terjadi, sedangkan jika dinyatakan non-statis (yaitu terlihat oleh unit kompilasi lain, dan karenanya berpotensi terkena efek samping dari fungsi eksternal seperti penataan ulang now ()) tidak terjadi.
Jeremy
3
@ Lưu Vĩnh Phúc: Kecuali bahwa panggilan itu sendiri tidak dibatalkan. Sekali lagi, saya menduga ini adalah karena compiler tidak tahu apa efek sampingnya mungkin - tetapi tidak tahu bahwa mereka efek samping tidak dapat mempengaruhi perilaku foo ().
Jeremy
3
Dan catatan terakhir: menentukan -flto (pengoptimalan waktu tautan) menyebabkan pengurutan ulang bahkan dalam kasus yang tidak diatur ulang.
Jeremy
20

Penyusunan ulang dapat dilakukan oleh kompiler, atau oleh prosesor.

Sebagian besar penyusun menawarkan metode khusus platform untuk mencegah pengurutan ulang instruksi baca-tulis. Di gcc, ini

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

( Informasi lebih lanjut di sini )

Perhatikan bahwa ini hanya secara tidak langsung mencegah operasi penataan ulang, selama operasi tersebut bergantung pada baca / tulis.

Dalam praktiknya saya belum melihat sistem di mana panggilan sistem Clock::now()memiliki efek yang sama seperti penghalang tersebut. Anda dapat memeriksa perakitan yang dihasilkan untuk memastikan.

Namun, tidak jarang fungsi yang diuji dievaluasi selama waktu kompilasi. Untuk menjalankan eksekusi "realistis", Anda mungkin perlu mendapatkan masukan foo()dari I / O atau volatilepembacaan.


Pilihan lain adalah menonaktifkan sebaris untuk foo()- sekali lagi, ini khusus untuk kompilator dan biasanya tidak portabel, tetapi akan memiliki efek yang sama.

Di gcc, ini akan menjadi __attribute__ ((noinline))


@Ruslan mengemukakan masalah mendasar: Seberapa realistis pengukuran ini?

Waktu eksekusi dipengaruhi oleh banyak faktor: satu adalah perangkat keras aktual tempat kita menjalankan, yang lainnya adalah akses bersamaan ke sumber daya bersama seperti cache, memori, disk dan inti CPU.

Jadi yang biasanya kami lakukan untuk mendapatkan pengaturan waktu yang sebanding : pastikan mereka dapat direproduksi dengan margin kesalahan rendah. Ini membuatnya agak artifisial.

Performa eksekusi "cache panas" vs. "cache dingin" dapat dengan mudah dibedakan berdasarkan urutan besarnya - tetapi pada kenyataannya, ini akan menjadi sesuatu di antara keduanya ("suam-suam kuku"?)

peterchen
sumber
2
Retasan Anda dengan asmmempengaruhi waktu eksekusi pernyataan antara panggilan timer: kode setelah pemanjat memori harus memuat ulang semua variabel dari memori.
Ruslan
@Ruslan: Hack mereka, bukan milikku. Ada berbagai tingkat pembersihan, dan melakukan hal seperti itu tidak dapat dihindari untuk hasil yang dapat direproduksi.
peterchen
2
Perhatikan bahwa peretasan dengan 'asm' hanya membantu sebagai penghalang untuk operasi yang menyentuh memori, dan OP tertarik lebih dari itu. Lihat jawaban saya untuk lebih jelasnya.
Chandler Carruth
11

Bahasa C ++ mendefinisikan apa yang bisa diamati dalam beberapa cara.

Jika foo()tidak ada yang bisa diamati, maka itu bisa dihilangkan sepenuhnya. Jika foo()hanya melakukan komputasi yang menyimpan nilai dalam status "lokal" (baik itu pada stack atau dalam objek di suatu tempat), dan compiler dapat membuktikan bahwa tidak ada pointer yang diturunkan dengan aman yang dapat masuk ke dalam Clock::now()kode, maka tidak ada konsekuensi yang dapat diamati untuk memindahkan Clock::now()panggilan.

Jika foo()berinteraksi dengan file atau layar, dan compiler tidak dapat membuktikan bahwa Clock::now()tidak tidak berinteraksi dengan file atau layar, maka penataan kembali tidak dapat dilakukan, karena interaksi dengan file atau tampilan adalah perilaku yang dapat diamati.

Meskipun Anda dapat menggunakan peretasan khusus kompiler untuk memaksa kode agar tidak berpindah-pindah (seperti perakitan inline), pendekatan lain adalah mencoba mengakali kompiler Anda.

Buat perpustakaan yang dimuat secara dinamis. Muat sebelum kode yang dimaksud.

Perpustakaan itu memperlihatkan satu hal:

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

dan membungkusnya seperti ini:

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 );
}

yang mengemas lambda nullary dan menggunakan pustaka dinamis untuk menjalankannya dalam konteks yang tidak dapat dipahami oleh compiler.

Di dalam perpustakaan dinamis, kami melakukan:

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

yang cukup sederhana.

Sekarang untuk menyusun ulang panggilan ke execute, itu harus memahami pustaka dinamis, yang tidak bisa dilakukan saat menyusun kode pengujian Anda.

Itu masih dapat menghilangkan foo()s tanpa efek samping, tetapi Anda menang beberapa, Anda kehilangan beberapa.

Yakk - Adam Nevraumont
sumber
19
"Pendekatan lain adalah mencoba mengakali penyusun Anda" Jika frasa itu bukan merupakan tanda telah jatuh ke lubang kelinci, saya tidak tahu apa itu. :-)
Cody Grey
1
Saya pikir mungkin akan membantu untuk mencatat bahwa waktu yang diperlukan untuk menjalankan blok kode tidak dianggap sebagai perilaku "yang dapat diamati" yang harus dipelihara oleh kompiler . Jika waktu untuk mengeksekusi blok kode "dapat diamati", maka tidak ada bentuk pengoptimalan kinerja yang diizinkan. Meskipun akan sangat membantu bagi C dan C ++ untuk mendefinisikan "penghalang kausalitas" yang akan memerlukan kompiler untuk menahan eksekusi kode apa pun setelah penghalang sampai semua efek samping dari sebelum penghalang ditangani oleh kode yang dihasilkan [kode yang mana ingin memastikan bahwa data telah sepenuhnya ...
supercat
1
... disebarkan melalui cache perangkat keras perlu menggunakan cara khusus perangkat keras untuk melakukan itu, tetapi cara khusus perangkat keras menunggu sampai semua tulis yang diposting selesai tidak akan berguna tanpa arahan penghalang untuk memastikan bahwa semua penulisan tertunda dilacak oleh kompiler harus diposting ke perangkat keras sebelum perangkat keras diminta untuk memastikan bahwa semua penulisan yang diposting telah lengkap.] Saya tidak tahu cara melakukan itu dalam bahasa apa pun tanpa menggunakan volatileakses tiruan atau panggilan ke kode luar.
supercat
4

Tidak, itu tidak bisa. Menurut standar C ++ [intro.execution]:

14 Setiap penghitungan nilai dan efek samping yang terkait dengan ekspresi penuh diurutkan sebelum setiap penghitungan nilai dan efek samping yang terkait dengan ekspresi penuh berikutnya yang akan dievaluasi.

Ekspresi penuh pada dasarnya adalah pernyataan yang diakhiri dengan titik koma. Seperti yang Anda lihat, aturan di atas menetapkan pernyataan harus dijalankan secara berurutan. Di dalam pernyataan itulah kompilator diizinkan lebih bebas kendali (yaitu dalam beberapa keadaan diizinkan untuk mengevaluasi ekspresi yang membuat pernyataan dalam urutan selain kiri-ke-kanan atau hal lain yang spesifik).

Perhatikan kondisi untuk menerapkan aturan seolah-olah tidak terpenuhi di sini. Tidak masuk akal untuk berpikir bahwa setiap kompilator akan dapat membuktikan bahwa pengubahan urutan panggilan untuk mendapatkan waktu sistem tidak akan memengaruhi perilaku program yang dapat diamati. Jika ada keadaan di mana dua panggilan untuk mendapatkan waktu dapat diatur ulang tanpa mengubah perilaku yang diamati, akan sangat tidak efisien untuk benar-benar menghasilkan kompilator yang menganalisis program dengan pemahaman yang cukup untuk dapat menyimpulkan ini dengan pasti.

Smeeheey
sumber
12
Masih ada aturan seolah
MM
18
Dengan as-if rule compiler dapat melakukan apa saja pada kode asalkan tidak mengubah perilaku yang dapat diamati. Waktu eksekusi tidak dapat diamati. Sehingga dapat menyusun ulang baris kode yang acak asalkan hasilnya sama (sebagian besar kompilator melakukan hal yang masuk akal dan tidak menyusun ulang panggilan waktu, tetapi tidak diperlukan)
Revolver_Ocelot
6
Waktu eksekusi tidak dapat diamati. Ini sangat aneh. Dari sudut pandang praktis dan non-teknis, waktu eksekusi (alias "kinerja") sangat bisa diamati.
Frédéric Hamidi
3
Tergantung bagaimana Anda mengukur waktu. Tidaklah mungkin untuk mengukur jumlah siklus jam yang dilakukan untuk mengeksekusi beberapa badan kode dalam C ++ standar.
Peter
3
@dba Anda mencampur beberapa hal bersama. Linker tidak lagi dapat menghasilkan aplikasi Win16, itu cukup benar, tetapi itu karena mereka telah menghapus dukungan untuk menghasilkan jenis biner tersebut. Aplikasi WIn16 tidak menggunakan format PE. Itu tidak berarti bahwa compiler atau linker memiliki pengetahuan khusus tentang fungsi API. Masalah lainnya terkait dengan pustaka runtime. Sama sekali tidak ada masalah mendapatkan versi terbaru dari MSVC untuk menghasilkan biner yang berjalan pada NT 4. Saya telah melakukannya. Masalahnya muncul segera setelah Anda mencoba menghubungkan di CRT, yang fungsi panggilannya tidak tersedia.
Cody Grey
2

Tidak.

Terkadang, dengan aturan "seolah-olah", pernyataan dapat diatur ulang. Ini bukan karena keduanya secara logis tidak bergantung satu sama lain, tetapi karena independensi tersebut memungkinkan penataan ulang semacam itu terjadi tanpa mengubah semantik program.

Memindahkan panggilan sistem yang memperoleh waktu saat ini jelas tidak memenuhi kondisi tersebut. Kompiler yang secara sadar atau tidak sadar melakukannya tidak patuh dan sangat konyol.

Secara umum, saya tidak akan mengharapkan ekspresi apa pun yang menghasilkan panggilan sistem menjadi "menebak-nebak" bahkan oleh compiler yang mengoptimalkan secara agresif. Itu hanya tidak cukup tahu tentang apa yang dilakukan oleh panggilan sistem itu.

Balapan Ringan dalam Orbit
sumber
5
Saya setuju bahwa ini akan konyol, tetapi saya tidak akan menyebutnya non-konforman . Penyusun dapat memiliki pengetahuan tentang apa yang disebut sistem pada sistem beton dan apakah itu memiliki efek samping. Saya berharap kompiler tidak menyusun ulang panggilan seperti itu hanya untuk mencakup kasus penggunaan umum, memungkinkan pengalaman pengguna yang lebih baik, bukan karena standar melarangnya.
Revolver_Ocelot
4
@Revolver_Ocelot: Pengoptimalan yang mengubah semantik program (oke, simpan untuk penghapusan salinan) tidak sesuai dengan standar, apakah Anda setuju atau tidak.
Balapan Ringan di Orbit
6
Dalam kasus sepele, int x = 0; clock(); x = y*2; clock();tidak ada cara yang ditentukan bagi clock()kode untuk berinteraksi dengan status x. Di bawah standar C ++, ia tidak harus tahu apa yang clock()dilakukannya - ia dapat memeriksa tumpukan (dan memperhatikan kapan komputasi terjadi), tetapi itu bukan masalah C ++ .
Yakk - Adam Nevraumont
5
Untuk mengambil poin Yakk lebih jauh: memang benar bahwa pengurutan ulang panggilan sistem, sehingga hasil yang pertama ditetapkan ke t2dan yang kedua ke t1, akan menjadi tidak sesuai dan konyol jika nilai-nilai itu digunakan, apa jawaban ini meleset adalah itu kompiler yang sesuai terkadang dapat mengatur ulang kode lain melalui panggilan sistem. Dalam hal ini, asalkan ia tahu apa yang foo()dilakukannya (misalnya karena ia telah membuat inline) dan karenanya (secara longgar) itu adalah fungsi murni maka ia dapat memindahkannya.
Steve Jessop
1
.. sekali lagi secara longgar, ini karena tidak ada jaminan bahwa implementasi aktual (meskipun bukan mesin abstrak) tidak akan menghitung secara spekulatif y*ysebelum panggilan sistem, hanya untuk kesenangan. Juga tidak ada jaminan bahwa implementasi sebenarnya tidak akan menggunakan hasil dari penghitungan spekulatif ini nanti pada titik mana pun xyang digunakan, oleh karena itu tidak melakukan apa pun di antara panggilan ke clock(). Hal yang sama berlaku untuk fungsi sebaris apa pun foo, asalkan tidak memiliki efek samping dan tidak dapat bergantung pada keadaan yang mungkin diubah oleh clock().
Steve Jessop
0

noinline function + kotak hitam perakitan inline + dependensi data lengkap

Ini didasarkan pada https://stackoverflow.com/a/38025837/895245 tetapi karena saya tidak melihat alasan yang jelas mengapa ::now()tidak dapat diatur ulang di sana, saya lebih suka menjadi paranoid dan memasukkannya ke dalam fungsi noinline bersama dengan asm.

Dengan cara ini saya cukup yakin pengubahan urutan tidak dapat terjadi, karena noinline"mengikat" ::nowdan ketergantungan data.

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 upstream .

Kompilasi dan jalankan:

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

Satu-satunya kelemahan kecil dari metode ini adalah kita menambahkan satu callqinstruksi tambahan di atas sebuah inlinemetode. objdump -CDacara yang mainberisi:

    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&)>

jadi kami melihat fooitu sebaris, tetapi get_clocktidak dan mengelilinginya.

get_clock Namun itu sendiri sangat efisien, terdiri dari instruksi yang dioptimalkan panggilan daun tunggal yang bahkan tidak menyentuh tumpukan:

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>

Karena ketepatan jam itu sendiri terbatas, saya pikir Anda tidak mungkin dapat melihat efek waktu dari satu ekstra jmpq. Perhatikan bahwa satu calltetap diperlukan karena ::now()berada di pustaka bersama.

Panggilan ::now()dari perakitan inline dengan ketergantungan data

Ini akan menjadi solusi yang paling efisien, mengatasi bahkan tambahan yang jmpqdisebutkan di atas.

Sayangnya ini sangat sulit dilakukan dengan benar seperti yang ditunjukkan di: Memanggil printf dalam ASM sebaris yang diperpanjang

Jika pengukuran waktu Anda dapat dilakukan langsung dalam perakitan inline tanpa panggilan, maka teknik ini dapat digunakan. Ini adalah kasus misalnya untuk instruksi instrumentasi sihir gem5 , x86 RDTSC (tidak yakin apakah ini mewakili lagi) dan mungkin penghitung kinerja lainnya.

Utas terkait:

Diuji dengan GCC 8.3.0, Ubuntu 19.04.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
1
Anda biasanya tidak perlu memaksa spill / reload dengan "+m", menggunakan "+r"adalah cara yang jauh lebih efisien untuk membuat compiler mewujudkan nilai dan kemudian mengasumsikan variabel telah berubah.
Peter Cordes