簡體   English   中英

是否允許編譯器優化堆內存分配?

[英]Is the compiler allowed to optimize out heap memory allocations?

考慮以下使用new簡單代碼(我知道沒有delete[] ,但它與這個問題無關):

int main()
{
    int* mem = new int[100];

    return 0;
}

是否允許編譯器優化new調用?

在我的研究中, g++ (5.2.0)和 Visual Studio 2015 不會優化new調用, 而 clang (3.0+) 會優化. 所有測試均已啟用完全優化(g++ 和 clang 為 -O3,Visual Studio 為 Release 模式)。

是不是new做一個系統調用引擎蓋下,使它不可能(和非法)的編譯器優化了這一點?

編輯:我現在已經從程序中排除了未定義的行為:

#include <new>  

int main()
{
    int* mem = new (std::nothrow) int[100];
    return 0;
}

clang 3.0 不再優化它,但更高的版本做了

編輯2

#include <new>  

int main()
{
    int* mem = new (std::nothrow) int[1000];

    if (mem != 0)
      return 1;

    return 0;
}

clang 總是返回 1

歷史似乎是 clang 遵循N3664: Clarifying Memory Allocation 中規定的規則,它允許編譯器圍繞內存分配進行優化,但正如Nick Lewycky 指出的那樣

Shafik 指出這似乎違反了因果關系,但 N3664 開始時是 N3433,我很確定我們先寫了優化,然后寫了論文。

因此,clang 實施了優化,后來成為作為 C++14 的一部分實施的提案。

基本問題是這是否是N3664之前的有效優化,這是一個棘手的問題。 我們將不得不轉到 C++ 標准草案第1.9程序執行中涵蓋的as-if 規則,其中說(強調我的):

本國際標准中的語義描述定義了一個參數化的非確定性抽象機器。 本國際標准對符合性實現的結構沒有要求。 特別是,他們不需要復制或模擬抽象機器的結構。 相反,符合要求的實現需要模擬(僅)抽象機的可觀察行為,如下所述。 5

注釋5說:

該規定有時被稱為“好像”規則,因為只要從可觀察到的行為可以確定的結果是好像要求已被遵守,實施就可以自由地無視本國際標准的任何要求的程序。 例如,如果一個實際的實現可以推斷出它的值沒有被使用並且沒有產生影響程序可觀察行為的副作用,那么它就不需要評估表達式的一部分。

由於new可能拋出一個具有可觀察行為的異常,因為它會改變程序的返回值,這似乎與as-if 規則所允許的相反

雖然,可以爭論何時拋出異常是實現細節,因此即使在這種情況下,clang 也可以決定它不會導致異常,因此省略new調用不會違反as-if 規則

as-if 規則下,優化掉對非拋出版本的調用似乎也是有效的。

但是我們可以在不同的翻譯單元中替換全局運算符 new ,這可能會導致這影響可觀察的行為,因此編譯器必須通過某種方式證明情況並非如此,否則它將無法執行此優化不違反as-if 規則 以前版本的 clang 確實在這種情況下進行了優化,因為這個 Godbolt 示例顯示了這是通過Casey here提供的,采用以下代碼:

#include <cstddef>

extern void* operator new(std::size_t n);

template<typename T>
T* create() { return new T(); }

int main() {
    auto result = 0;
    for (auto i = 0; i < 1000000; ++i) {
        result += (create<int>() != nullptr);
    }

    return result;
}

並將其優化為:

main:                                   # @main
    movl    $1000000, %eax          # imm = 0xF4240
    ret

這確實看起來過於激進,但后來的版本似乎沒有這樣做。

這是N3664允許的。

允許實現省略對可替換全局分配函數的調用(18.6.1.1、18.6.1.2)。 當它這樣做時,存儲由實現提供或通過擴展另一個新表達式的分配來提供。

此提案是C ++ 14標准的一部分,因此,在C ++ 14編譯器允許優化了new式(即使它可能拋出)。

如果您查看Clang 實現狀態,它清楚地表明他們確實實現了 N3664。

如果您在 C++11 或 C++03 中編譯時觀察到這種行為,您應該填補一個錯誤。

請注意,在 C++14 之前,動態內存分配程序可觀察狀態一部分(盡管我目前找不到相關的參考),因此不允許符合的實現在此應用as-if規則案件。

請記住,C++ 標准告訴正確的程序應該做什么,而不是它應該如何做。 它完全不能告訴后者,因為新架構可以並且確實在標准編寫之后出現並且標准必須對他們有用。

new不必是引擎蓋下的系統調用。 有些計算機可以在沒有操作系統和系統調用概念的情況下使用。

因此,只要最終行為不改變,編譯器就可以優化任何東西。 包括那個new

有一個警告。
可以在不同的翻譯單元中定義替換全局運算符 new
在這種情況下, new 的副作用可能無法優化掉。 但是如果編譯器可以保證 new 運算符沒有副作用,就像發布的代碼是整個代碼的情況一樣,那么優化是有效的。
這個 new 可以拋出 std::bad_alloc 不是必需的。 在這種情況下,在優化new時,編譯器可以保證不會拋出異常,也不會產生副作用。

編譯器完全允許(但不是必需)優化原始示例中的分配,在標准第 1.9 節的 EDIT1 示例中更是如此,這通常稱為as-if 規則

符合要求的實現需要模擬(僅)抽象機的可觀察行為,如下所述:
[3頁條件]

cppreference.com上提供了一種更易讀的表示。

相關要點是:

  • 您沒有揮發性物質,因此 1) 和 2) 不適用。
  • 您不輸出/寫入任何數據或提示用戶,因此 3) 和 4) 不適用。 但即使你這樣做了,他們顯然會在 EDIT1 中得到滿足(可以說在最初的例子中也是如此,雖然從純理論的角度來看,這是非法的,因為程序流程和輸出 - 理論上 - 不同,但請參閱兩段以下)。

異常,即使是未捕獲的異常,也是定義良好(不是未定義!)的行為。 然而,嚴格來說,如果new拋出(不會發生,另見下一段),可觀察到的行為會有所不同,無論是程序的退出代碼還是程序后面可能出現的任何輸出。

現在,在單個小分配的特殊情況下,您可以為編譯器提供“懷疑的好處” ,即它可以保證分配不會失敗。
即使在內存壓力非常大的系統上,當您的可用分配粒度小於最小分配粒度時,甚至無法啟動進程,並且堆也將在調用main之前設置。 因此,如果此分配失敗,則程序將永遠不會啟動,或者甚至在調用main之前就已經遇到了不正常的結束。
就此而言,假設編譯器知道這一點,即使理論上分配可能會拋出,甚至優化原始示例也是合法的,因為編譯器實際上可以保證它不會發生。

<有點猶豫>
另一方面,在您的 EDIT2 示例中優化分配是不允許的(正如您所觀察到的,編譯器錯誤)。 消耗該值以產生外部可觀察的效果(返回碼)。
請注意,如果您將new (std::nothrow) int[1000]替換為new (std::nothrow) int[1024*1024*1024*1024ll] (這是 4TiB 分配!),即——在當今的計算機上-- 保證失敗,它仍然優化調用。 換句話說,盡管您編寫的代碼必須輸出 0,但它返回 1。

@Yakk 對此提出了一個很好的論據:只要永遠不會觸及內存,就可以返回一個指針,而不需要實際的 RAM。 在此范圍內,優化 EDIT2 中的分配甚至是合法的。 我不確定這里誰對誰錯。

在一台機器上執行 4TiB 分配幾乎肯定會失敗,因為操作系統需要創建頁表,而機器至少沒有 2 位數 GB 的 RAM。 現在當然,C++ 標准不關心頁表或操作系統正在做什么來提供內存,這是真的。

但另一方面,假設“如果不觸及內存,這將起作用”確實依賴於這樣的細節和操作系統提供的東西。 如果未觸及 RAM,則實際上不需要它的假設僅是正確的,因為操作系統提供了虛擬內存。 這意味着操作系統需要創建頁表(我可以假裝我不知道它,但這並沒有改變我依賴它的事實)。

因此,我認為首先假設一個然后說“但我們不關心另一個”並不是 100% 正確的。

所以,是的,編譯器可以假設,只要不觸及內存,4TiB 分配通常是完全可能的,並且它可以假設通常有可能成功。 它甚至可能會假設它很可能會成功(即使不是)。 但我認為,無論如何,當有失敗的可能性時,你永遠不能假設某些東西必須起作用。 不僅有失敗的可能性,在那個例子中,失敗的可能性更大
</有點猶豫>

在您的代碼段中可能發生的最壞情況是new拋出std::bad_alloc ,這是未處理的。 然后發生的事情是實現定義的。

最好的情況是無操作,最壞的情況沒有定義,編譯器可以將它們分解為不存在。 現在,如果您真的嘗試捕獲可能的異常:

int main() try {
    int* mem = new int[100];
    return 0;
} catch(...) {
  return 1;
}

...然后保持對operator new的調用

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM