[英]Why are gcc and clang not hoisting strlen out of this loop?
考慮以下代碼:
#include <string.h>
void bar(char c);
void foo(const char* restrict ss) {
for (int i = 0; i < strlen(ss); ++i) {
bar(*ss);
}
}
我希望在這些基本理想的條件下將strlen(ss)
提升出循環; 然而 -它不是,無論是 clang 5.0 還是 gcc 7.3的最大優化( -O3
)。
為什么會這樣?
注意:靈感來自(我對這個問題的回答)。
其他答案聲稱無法提升strlen
調用,因為字符串的內容可能會在調用之間發生變化。 這些答案並沒有正確解釋restrict
的語義; 即使bar
可以通過全局變量或其他某種機制訪問字符串, restrict
指向const
類型的指針的語義應該(請參閱警告)禁止bar
修改字符串。
1 令 D 是一個普通標識符的聲明,它提供了一種將對象 P 指定為類型 T 的限制限定指針的方法。
2 如果 D 出現在塊內並且沒有存儲類 extern,則讓 B 表示該塊。 如果 D 出現在函數定義的參數聲明列表中,讓 B 表示關聯的塊。 否則,讓 B 表示 main 的塊(或在獨立環境中程序啟動時調用的任何函數的塊)。
3 在下文中,如果(在對 E 求值之前執行 B 的某個序列點)修改 P 以指向它之前所在的數組對象的副本,則稱指針表達式 E 基於對象 P Pointed 會改變 E 的值。 137)請注意,''based'' 只為具有指針類型的表達式定義。
4 在B的每次執行過程中,設L為任何基於P的具有&L的左值。如果L用於訪問它指定的對象X的值,並且X也被修改(通過任何方式),那么以下要求apply:T 不應是 const 限定的。 用於訪問 X 值的每個其他左值也應具有基於 P 的地址。就本條而言,修改 X 的每個訪問也應視為修改 P。 如果 P 被分配了基於另一個受限指針對象 P2 的指針表達式 E 的值,與塊 B2 相關聯,則 B2 的執行應在 B 的執行之前開始,或者 B2 的執行應在 B2 的執行之前結束。分配。 如果不滿足這些要求,則行為未定義。
5 這里 B 的執行意味着程序執行的一部分,該部分將對應於具有與 B 相關聯的標量類型和自動存儲持續時間的對象的生命周期。
這里,聲明D
是const char* __restrict__ ss
,關聯的塊B
是foo
的主體。 strlen
通過其訪問字符串的所有&L
都有基於ss
&L
(請參閱警告) ,並且這些訪問發生在B
的執行期間(因為,根據第 5 節中的定義, strlen
的執行是B
執行的一部分)。 ss
指向一個 const 限定的類型,因此在第 4 節中,編譯器可以假設strlen
訪問的字符串元素在foo
執行期間沒有被修改; 修改它們將是未定義的行為。
(警告)上述分析假設strlen
通過“普通”指針解引用或索引訪問字符串。 如果strlen
使用諸如 SSE 內在函數或內聯匯編之類的技術,我不清楚這種訪問在技術上是否算作使用左值訪問它指定的對象的值。 如果它們不是這樣,則restrict
的保護可能不適用,並且編譯器可能無法執行提升。
也許上述警告使restrict
的保護無效。 也許編譯器對strlen
的定義不夠了解,無法分析其與restrict
交互(我很驚訝它沒有內聯)。 也許編譯器可以自由地執行提升,只是沒有意識到; 也許一些相關的優化沒有實現,或者它未能在正確的編譯器組件之間傳播必要的信息。 確定確切原因需要比我更熟悉 GCC 和 Clang 內部結構。
消除strlen
和循環的進一步簡化測試表明,Clang 確實對限制指針到常量優化提供了一些支持,但我無法觀察到 GCC 的任何此類支持。
ss
可能是某個全局變量,因為您可以使用一些全局數組(如char str[100];
調用foo
char str[100];
作為它的論點(例如通過在你的main
有foo(str);
)...
並且bar
可以修改該全局變量(然后strlen(ss)
可以在每個循環中更改)。
順便說一句, restrict
可能不是你相信的意思。 仔細閱讀 C11 標准的第6.7.3節和第6.7.3.1 節。 恕我直言, restrict
實際上在同一函數的兩個正式參數上最有用,以表達它們不是“別名”或“重疊”指針的事實(如果您猜到我的真正意思),也許對restrict
優化工作可能已經專注於此類案件。
也許(但不太可能),在您的特定程序上,如果您將編譯器作為gcc -flto -fwhole-program -O3
調用(在每個翻譯單元和程序鏈接時),它可能會根據您的需要進行優化。 我不會打賭(但我讓你檢查)。
為什么會出現這種情況?
至於為什么當前的GCC (或Clang )沒有像你想要的那樣優化,那是因為沒有人寫過這樣的優化過程並在-O3
啟用它。
編譯器不需要做優化,只允許做一些優化(由他們的實現者選擇)。
由於它是免費軟件,請隨時通過為 GCC (或 Clang)做出貢獻來提出補丁。 您可能需要一整年的工作,並且不確定您的優化是否會被接受(因為實際上沒有人像您展示的代碼一樣,或者因為您的優化太具體,所以不太可能被觸發,但仍然會減慢編譯器)。 但歡迎您嘗試。
即使§6.7.3.1允許您進行優化(如user2357112的回答所示),它實際上可能不值得努力實現它。
(我的直覺是實現這樣的優化很困難,並且在實踐中對現有程序不會有太大好處)
順便說一句,你絕對可以通過編寫一些GCC 插件來嘗試這樣的優化(因為插件框架是為這樣的實驗而設計的)。 您可能會發現對這樣的優化進行編碼需要大量工作,實際上它並沒有提高大多數現有程序(例如在 Linux 發行版中)的性能,因為很少有人以這種方式編碼。
GCC 和 Clang 都是自由軟件項目,它們的貢獻者是(從 FSF 的角度來看)志願者。 因此,您可以隨意改進 GCC (或 Clang),就像您希望它優化. 根據以往的經驗,即使是向 GCC 貢獻一小段代碼也需要花費大量時間。 而 GCC 是一個龐大的程序(約千萬行代碼),因此了解其內部結構並不容易。
由於strlen
正在傳遞一個指針,並且它所指向的內存內容可能會在對strlen
的調用之間發生變化,因此優化調用可能會引入錯誤。 如果您可以向 gcc 保證該函數將始終返回相同的值,它將對其進行優化。 來自函數屬性文檔:
常量
許多函數除了它們的參數之外不檢查任何值,並且除了返回一個值之外沒有任何作用。 對此類函數的調用有助於優化,例如消除公共子表達式。 const 屬性比下面類似的純屬性對函數的定義施加了更大的限制,因為它禁止函數讀取全局變量。 因此,函數聲明中屬性的存在允許 GCC 為某些函數調用發出更有效的代碼。 診斷使用 const 和 pure 屬性裝飾相同的函數。
所以strlen
對strlen
的外部依賴,看看下面兩個編譯的區別:
int baz (const char* s) __attribute__ ((pure));
void foo(const char* __restrict__ ss)
{
for (int i = 0; i < baz(ss); ++i)
bar(*ss);
}
產量:
foo:
push rbp
push rbx
mov rbp, rdi
xor ebx, ebx
sub rsp, 8
jmp .L2
.L3:
movsx edi, BYTE PTR [rbp+0]
add ebx, 1
call bar
.L2:
mov rdi, rbp
call baz
cmp eax, ebx
jg .L3
add rsp, 8
pop rbx
pop rbp
ret
但是,如果我們將baz
上的pure
屬性更改為const
您可以看到調用已從循環中提升:
foo:
push r12
push rbp
mov r12, rdi
push rbx
xor ebx, ebx
call baz
mov ebp, eax
jmp .L2
.L3:
movsx edi, BYTE PTR [r12]
add ebx, 1
call bar
.L2:
cmp ebp, ebx
jg .L3
pop rbx
pop rbp
pop r12
ret
所以也許可以搜索你的頭文件,看看strlen
是如何聲明的。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.