我發現this post,寫一些像這樣的測試:C++編譯器如何優化堆棧分配?
我期待編譯器使得TCO上foo3
,,破壞sp
第一和調用func
用一個簡單的跳躍,不會創建堆棧幀。但它沒有發生。該程序在(彙編代碼)第47行運行到func
,之後是call
和清除sp
對象。即使我清除~Simple()
,優化也不會發生。
那麼,在這種情況下如何觸發TCO?
我發現this post,寫一些像這樣的測試:C++編譯器如何優化堆棧分配?
我期待編譯器使得TCO上foo3
,,破壞sp
第一和調用func
用一個簡單的跳躍,不會創建堆棧幀。但它沒有發生。該程序在(彙編代碼)第47行運行到func
,之後是call
和清除sp
對象。即使我清除~Simple()
,優化也不會發生。
那麼,在這種情況下如何觸發TCO?
首先,請注意該示例有一個雙免費的錯誤。如果移動構造函數被調用,sp.buffer
沒有被設置爲nullptr
,因爲它必須是兩個指向緩衝區的指針才能被刪除。其正確管理指針簡單的版本是:
struct Simple {
std::unique_ptr<int[]> buffer {new int[1000]};
};
隨着該修補程序,讓內聯幾乎所有的東西,看看foo3
確實在其所有的榮耀:
using func_t = std::function<int(Sample&&)>&&;
int foo3(func_t func) {
int* buffer1 = new int[1000]; // the unused local
int* buffer2 = new int[1000]; // the call argument
if (!func) {
delete[] buffer2;
delete[] buffer1;
throw bad_function_call;
}
try {
int retval = func(buffer2); // <-- the call
} catch (...) {
delete[] buffer2;
delete[] buffer1;
throw;
}
delete[] buffer2;
delete[] buffer1;
return retval; // <-- the return
}
的buffer1
的情況下,很簡單。它是一個未使用的本地,唯一的副作用是分配和釋放,編譯器被允許跳過。一個足夠智能的編譯器可以完全刪除未使用的本地。鏗鏘聲++ 5.0似乎完成了這一點,但g ++ 7.2沒有。
更有趣的是buffer2
。 func
採用非const的右值引用。它可以修改參數。例如,它可能會離開它。但它可能不會。臨時可能仍然擁有一個緩衝區,必須在通話後刪除,foo3
必須這樣做。電話是而不是一個尾巴呼叫。
正如觀察,我們通過簡單的泄漏緩衝更接近尾調用:
struct Simple {
int* buffer = new int[1000];
};
這是欺騙了一下,因爲這個問題的很大一部分是關於在平凡的析構函數的臉尾部調用優化。但讓我們來欣賞這一點。正如所觀察到的,這不會導致尾聲。
首先,請注意,按引用傳遞是傳遞指針的一種奇特形式。該對象仍然必須存在於某個地方,並且位於調用者的堆棧中。在呼叫期間保持呼叫者的堆疊存活並且非空,將排除尾部呼叫優化。
爲了啓用尾部呼叫,我們希望在寄存器中傳遞func
的參數,因此它不必住在foo3
的堆棧中。這表明,我們應該按值傳遞:
int foo2(Simple); // etc.
SysV的ABI決定了要在寄存器中傳遞,它必須是可拷貝平凡,可移動和破壞。作爲一個結構包裝int*
,我們已經涵蓋。有趣的事實:我們不能在這裏使用std::unique_ptr
,因爲它不是一般的可破壞的。
即使如此,我們仍然看不到尾巴呼叫。我沒有看到阻止它的原因,但我不是專家。用函數指針替換std::function
確實會導致尾部調用。 std::function
在調用中有一個額外的參數,並有條件投擲。這是否有可能使其難以優化?
反正,用一個函數指針,克++ 7.2和鐺++ 5.0做尾調用:
struct Simple {
int* buffer = new int[1000];
};
int foo2(Simple sp) {
return sp.buffer[std::rand()];
}
using func_t = int (*)(Simple);
int foo3(func_t func) {
return func(Simple());
}
但是,這是泄漏。我們可以做得更好嗎?有這種類型的所有權,我們希望將它從foo3
傳遞到func
。但是具有非平凡析構函數的類型不能在參數中傳遞。這意味着像std::unique_ptr
這樣的RAII類型不會讓我們在那裏。從GSL使用的一個概念,我們至少可以表達所有權:
template<class T> using owner = T;
struct Simple {
owner<int*> buffer = new int[1000];
};
然後,我們可以希望,靜態分析工具,現在或將來可以檢測foo2
被接受的所有權,但從來沒有刪除buffer
。
老兄,您需要直接看一些程序集,而不是查看打印語句。打印語句的存在可能會改變優化的方式。我建議https://godbolt.org/。 –
您無法可靠地對兩個不相關的指針進行算術運算,就像您在例如'print_mem'函數。這種方式存在[*未定義的行爲*](http://en.cppreference.com/w/cpp/language/ub)。 –
@NirFriedman謝謝你的建議。我剛剛嘗試過並更新了我的問題。 – Cowsay