各種語言都行。

編譯一個可執行程序的時候,好像判斷一個函數是否被使用也不是很困難,像GC那樣從main判斷可達性貌似就可以了。

編譯類庫的時候,那些private的函數好像也是可以去掉的。

編譯器會把沒使用的函數刪除掉嗎?(雖說對於手機、PC、伺服器來說不算什麼,但是對於嵌入式來說可能就比較有用了)

退一步講,有沒有編譯器會對未使用的函數給出警告/提示呢?


別的編譯器不知道, gcc打開編譯參數-ffunction-sections和-fdata-sections, 鏈接參數 -Wl,--gc-sections就行了, 源程序里沒有被調用到的函數/靜態數據不會鏈接到最終的執行文件. 不開的話就真的都編譯鏈接進去了.

sdcc至今沒加上這個功能, 編譯出來的二進位代碼就大得多, 所以許多sdcc下的庫都拆成了每個文件一個函數的形式.

至於警告... static函數如果沒有用到, 編譯時會有warning. 非static函數的話, 編譯階段沒法知道是否在外部調用了, 所以沒法報警了.


VC++:release會刪。

Delphi 7:有時候刪的有點多,會導致程序出問題。

C#:刪是不可能刪的,萬一要反射怎麼辦?


答案先復現效果,然後介紹原理。

例如拿c/c++驗證下你的想法:

//a.c
static void test() {}
static void test2() {}
int main() {
test();
}

static是為了防止被外部引用,輔助編譯器判斷是否unused,編譯:

clang -Wunused-function a.c
a.c:2:13: warning: unused function test2 [-Wunused-function]
static void test2() {}
^
1 warning generated.

對於c++,

//a.cc
namespace {
class A {
private:
void test2() {};
public:
void test() {};

};
}
int main() {
auto a = A();
a.test();
}

對於c++,我們直接看其編譯出來的ir文件,編譯:

$ clang++ -S -emit-llvm -Wunused-member-function a.cc
a.cc:5:14: warning: unused member function test2 [-Wunused-member-function]
void test2() {};
^
1 warning generated.

$ cat a.ll

....
; Function Attrs: noinline norecurse optnone ssp uwtable
define i32 @main() #0 {
%1 = alloca %class.A, align 1
%2 = bitcast %class.A* %1 to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* align 1 %2, i8* align 1 getelementptr inbounds (%class.A, %class.A* @__const.main.a, i32 0, i32 0), i64 1, i1 false)
call void @_ZN1A4testEv(%class.A* %1)
ret i32 0
}

; Function Attrs: argmemonly nounwind
declare void @llvm.memcpy.p0i8.p0i8.i64(i8* nocapture writeonly, i8* nocapture readonly, i64, i1) #1

; Function Attrs: noinline nounwind optnone ssp uwtable
define linkonce_odr void @_ZN1A4testEv(%class.A*) #2 align 2 {
%2 = alloca %class.A*, align 8
store %class.A* %0, %class.A** %2, align 8
%3 = load %class.A*, %class.A** %2, align 8
ret void
}
....

可見,編譯會報warning,其次是ir裡面是找不到包含test2字元串的函數定義的,也就是說被直接優化掉了。 匿名namespace 跟static的作用一樣。參考[1].

所以可以看到,unused函數/方法確實會被清理掉。

再看下清理的原理, 還是拿clang/clang++ 和llvm來說明。無用方法清理會發生在2個階段:

  1. 編譯優化階段: 例如上面的2個例子,都是編譯階段直接能夠判斷出來的是否能夠被外部訪問,static和匿名namespace就是防止外部文件對該函數進行訪問,進一步本文件內部也沒有使用,就可以通過DCE(死代碼消除)-不可達代碼優化實現,具體原理可以見任何一本編譯原理的數據流分析的不可達分析。
  2. 鏈接優化階段(LTO): 例如在編譯階段使用選項-ffunction-sections, -fno-function-sections , 參考[2]. 這個選項將給每個函數分配獨立的section,然後在鏈接階段通過`-dead_strip` 選項清理無用section。 ThinLTO使用index based dead symbol analysis,從導出符號出發,遍歷其引用圖,篩選被引用的符號,生成最後的目標代碼。論文參考[3],代碼參考[4]。

參考:

[1]: https://stackoverflow.com/questions/154469/unnamed-anonymous-namespaces-vs-static-functions

[2] : https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-ffunction-sections

[3]: https://storage.googleapis.com/pub-tools-public-publication-data/pdf/af0a39422b19fbbe063479f5d3a71d9278677314.pdf

[4]: http://llvm-cs.pcc.me.uk/lib/LTO/LTO.cpp#1260


給你見識一下 Rust 編譯器:

好巧不巧知乎圖片系統升級發不了圖片了。。。自己運行一下吧:https://play.rust-lang.org/?version=stablemode=releaseedition=2018gist=76b346fece29f0cc2eeafda2d2c0fec0

點這個鏈接過去,執行Run。

代碼說明:

fn add_one(i: mut u32) {
*i += 1;
}

fn main(){
let mut a = 2;
// add_one(mut a);
println!("{:?}", a);
}

看到沒? add_one 函數沒有被用到。

編譯器輸出:

warning: variable does not need to be mutable
--&> src/main.rs:6:9
|
6 | let mut a = 2;
| ----^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default

warning: function is never used: `add_one`
--&> src/main.rs:1:4
|
1 | fn add_one(i: mut u32) {
| ^^^^^^^
|
= note: `#[warn(dead_code)]` on by default

看第二條警告!

如果你在代碼頭部增加 #[deny(dead_code)] ,則這個警告會變成 Error。如果你使用 #[allow(dead_code)],則不會發生任何警告和Error。

編譯器在編譯的時候通過靜態分析就能判斷哪個方法被用了哪個沒被用。只不過 Rust 編譯器(即,rustc)是做的最好的一個。

Rust 編譯過程是「源碼文本 -&> Tokens -&> AST -&> HIR -&> MIR -&> LLVM IR -&> LLVM-&> 機器碼」,在 HIR(高級中間語言)階段,就會做這個事情。 你定義的函數在編譯器編譯過程中都會產生唯一id,在 HIR 階段會檢查這些 id,判斷它們是否有被調用到。


go:報錯!不讓你存在未使用的函數!


這種東西要查編譯器手冊的,地球上的編譯器太多了數不過來。以RealView MDK(小白們說的Keil)為例,C編譯選項有one ELF per function,勾上這個選項,那些沒有用到的函數會被直接丟棄,不會出現在最終代碼裡面。如果不勾上,只要c文件中的一個函數是最終代碼需要的,那麼這個文件裡面所有的函數都會出現在最終代碼裡面。


編譯,但可能不鏈接。視鏈接器配置。


我的回答還是基於多年前我的C、C++經驗,現在是否過時,就不得而知了。

以C為例,一個.c文件中的所有定義,包括函數、模塊變數等,都在一個 .obj 文件中,因為無法知道哪些會被其他模塊使用,因此,obj文件中是包括全部定義,不可以進行刪除的。

而 Link 過程中,則是以 obj 為基礎,將多個 obj 鏈接成為一個 目標執行文件(或者.so),這個過程中,也是以 obj 為單位,而不是以函數為單位的。除非一個 obj 中的全部定義都不被使用,否則,整個 obj 都會被鏈接到目標文件中。這也是很多 C library中,會將原代碼拆分為很多小的c文件原因。

不清楚現在的編譯技術是否已經可以做到以函數為單位進行鏈接了?如果這樣做的話,似乎也不是很困難,相當於編譯器將每一個函數都編譯為一個.obj文件。但共享的模塊變數如何拆分,就不清楚了。


不同語言不太一樣,有反射的可能不會刪。如果要優化掉,可以從入口函數開始 travers 整個語法樹,把遇到的所有function node丟進新的sequence,然後再把老的sequence丟掉。如果要加warning什麼的,比一下兩個sequence就行了吧。


推薦閱讀:
相关文章