我在一段C/C++代碼中遇到了一段非常奇怪的表現行爲,正如標題中所建議的那樣,我不知道該如何解釋。添加打印語句會使代碼速度加快一個數量級
這裏是一個爲緊密型的 - 我發現的到最小的工作示例[編輯:請參閱下面的一個較短]:
#include <stdio.h>
#include <stdlib.h>
#include <complex>
using namespace std;
const int pp = 29;
typedef complex<double> cdbl;
int main() {
cdbl ff[pp], gg[pp];
for(int ii = 0; ii < pp; ii++) {
ff[ii] = gg[ii] = 1.0;
}
for(int it = 0; it < 1000; it++) {
cdbl dual[pp];
for(int ii = 0; ii < pp; ii++) {
dual[ii] = 0.0;
}
for(int h1 = 0; h1 < pp; h1 ++) {
for(int h2 = 0; h2 < pp; h2 ++) {
cdbl avg_right = 0.0;
for(int xx = 0; xx < pp; xx ++) {
int c00 = xx, c01 = (xx + h1) % pp, c10 = (xx + h2) % pp,
c11 = (xx + h1 + h2) % pp;
avg_right += ff[c00] * conj(ff[c01]) * conj(ff[c10]) * gg[c11];
}
avg_right /= static_cast<cdbl>(pp);
for(int xx = 0; xx < pp; xx ++) {
int c01 = (xx + h1) % pp, c10 = (xx + h2) % pp,
c11 = (xx + h1 + h2) % pp;
dual[xx] += conj(ff[c01]) * conj(ff[c10]) * ff[c11] * conj(avg_right);
}
}
}
for(int ii = 0; ii < pp; ii++) {
dual[ii] = conj(dual[ii])/static_cast<double>(pp*pp);
}
for(int ii = 0; ii < pp; ii++) {
gg[ii] = dual[ii];
}
#ifdef I_WANT_THIS_TO_RUN_REALLY_FAST
printf("%.15lf\n", gg[0].real());
#else // I_WANT_THIS_TO_RUN_REALLY_SLOWLY
#endif
}
printf("%.15lf\n", gg[0].real());
return 0;
}
這裏是運行這個結果我係統:
[email protected] $ g++ -o test.elf test.cc -Wall -Wextra -O2
[email protected] $ time ./test.elf > /dev/null
real 0m7.329s
user 0m7.328s
sys 0m0.000s
[email protected] $ g++ -o test.elf test.cc -Wall -Wextra -O2 -DI_WANT_THIS_TO_RUN_REALLY_FAST
[email protected] $ time ./test.elf > /dev/null
real 0m0.492s
user 0m0.490s
sys 0m0.001s
[email protected] $ g++ --version
g++ (Gentoo 4.9.4 p1.0, pie-0.6.4) 4.9.4 [snip]
這不是什麼代碼是計算非常重要:它只是一個在長度29的陣列複雜運算的每噸它已經「簡化」從複雜的算術大得多噸,我所關心的。
所以,行爲似乎是,如標題所述:如果我把這個打印語句放回去,代碼會快得多。
我玩過一段時間:例如,打印常量字符串不會提高加速度,但打印時鐘的時間確實如此。有一個非常明確的門檻:代碼是快或慢。
我考慮過一些奇怪的編譯器優化可能會啓動或不啓動的可能性,也許取決於代碼是否有副作用。但是,如果是這樣,它非常微妙:當我查看反彙編的二進制文件時,它們看起來是相同的,只是它有一個額外的打印語句,它們使用不同的可互換寄存器。我可能(必須)錯過了一些重要的東西。
我完全喪失解釋什麼是地球可能造成這種情況。更糟糕的是,它的確影響了我的生活,因爲我正在運行相關的代碼,並且插入額外的打印語句並不是一個好的解決方案。
任何可能的理論都會受到歡迎。如果你可以解釋如何解釋任何事情,那麼可以接受「你的電腦壞了」的回答。
UPDATE:有道歉的問題越來越長,我已經縮水的例子
#include <stdio.h>
#include <stdlib.h>
#include <complex>
using namespace std;
const int pp = 29;
typedef complex<double> cdbl;
int main() {
cdbl ff[pp];
cdbl blah = 0.0;
for(int ii = 0; ii < pp; ii++) {
ff[ii] = 1.0;
}
for(int it = 0; it < 1000; it++) {
cdbl xx = 0.0;
for(int kk = 0; kk < 100; kk++) {
for(int ii = 0; ii < pp; ii++) {
for(int jj = 0; jj < pp; jj++) {
xx += conj(ff[ii]) * conj(ff[jj]) * ff[ii];
}
}
}
blah += xx;
printf("%.15lf\n", blah.real());
}
printf("%.15lf\n", blah.real());
return 0;
}
我可以使它更小,但已經機器碼是可控的。如果我將與第一個printf的callq指令對應的二進制文件的第二個字節更改爲0x90,則執行從快速變爲慢。
對__muldc3()進行函數調用的編譯代碼非常繁重。我認爲這必須與Broadwell體系結構如何處理或不處理這些跳躍有關:兩個版本的指令數相同,因此它們在指令/週期中有所不同(約0.16比2.8)。
此外,編譯-static使事情再次變得更快。
而且無恥更新:我自覺我是唯一一個誰可以玩這個,所以這裏有一些更多的觀測:
好像調用任何庫函數—包括一些愚蠢的我編造的什麼也沒做—爲第一次,把執行進入緩慢狀態。隨後對printf,fprintf或sprintf的調用以某種方式清除狀態,並且執行速度又很快。所以,重要的是,第一次調用__muldc3()時,我們進入緩慢狀態,下一個{,f,s} printf重置所有內容。
一旦一個庫函數被調用一次,並且狀態已被重置,該函數就變成了空閒狀態,並且可以在不改變狀態的情況下儘可能多地使用它。
因此,例如:
#include <stdio.h>
#include <stdlib.h>
#include <complex>
using namespace std;
int main() {
complex<double> foo = 0.0;
foo += foo * foo; // 1
char str[10];
sprintf(str, "%c\n", 'c');
//fflush(stdout); // 2
for(int it = 0; it < 100000000; it++) {
foo += foo * foo;
}
return (foo.real() > 10.0);
}
是快,但註釋掉線1或取消註釋第2行使得它再次放緩。
第一次運行庫調用時,PLT中的「蹦牀」被初始化爲指向共享庫必須相關。所以,也許不知何故,這個動態加載代碼將處理器前端留在不好的地方,直到它被「救出」。
如果您在不使用printf語句的情況下調用'gg [0] .real()',那麼您是否會看到相同的行爲(以這種方式它沒有被優化掉)。我嘗試了你的代碼,但是在我的系統中沒有看到相同的行爲(兩個版本都是同一時間),但是這是虛擬機,因此可能會有不同的表現。 –
無法重現:https://ideone.com/DuAYHz,https://ideone.com/BY4GIq – 2501
我無法重現這一點,兩者在我的系統(gcc6,64位Linux)上幾乎同樣快。你能提供關於你的設置的更多信息嗎? –