簡體   English   中英

C優化問題

[英]C optimization question

我想知道什么是我編寫某些代碼最快的方法。 我有一個循環,在一些整數上執行加法。 該循環將執行很多次,因此我考慮過進行比較以檢查是否有任何操作數為零,因此不應將它們視為相加,如下所示:

if (work1 == 0)
{
    if (work2 == 0)
        tempAnswer = toCarry;
    else
        tempAnswer = work2 + toCarry; 
}
else if (work2 == 0)
    tempAnswer = work1 + toCarry;
else
    tempAnswer = work1 + work2 + toCarry;

我相信頂部的嵌套IF已經是一種優化,因為它比與&&進行一系列比較要快,因為我要檢查(work1 == 0)不止一次。

令人遺憾的是,我無法說出work1和work2多久為零,因此假設它很可能是IF語句的每個可能結果的平衡分布。

因此,鑒於此,以上代碼是否比僅編寫tempAnswer = work1 + work2 + toCarry還是所有比較都可能導致大量拖累?

謝謝

廢話

  • 比較兩個整數只需要兩個整數相加即可。
  • 進行分支的時間比添加的時間長得多(在許多方面,公認的更舊(請參見注釋),CPU)
  • 在更現代的體系結構上,瓶頸是從內存訪問值,因此該方案仍無法在需要的地方提供幫助。

    另外,從邏輯上考慮一下-為什么將零視為您認為是特例的一個值? 為什么不同時檢查一個,並使用tempAnswer++ 當您考慮所有可能性時,您會發現這是沒有意義的練習。

與往常一樣,答案是分析您的代碼 用兩種方式編寫它,計時,然后看哪個更快。

就是說,我的錢將直接比一堆比較快。 每個比較都意味着潛在的分支,分支可能會對處理器中的流水線造成嚴重破壞。

分支最有可能比添加慢,因此這可能適得其反。 無論如何,它都很難閱讀。 在沒有確鑿的證據表明您需要它之前,您真的不應該嘗試將其優化到這個水平。 對代碼的負面影響通常是不值得的。

不,不是更快。 分支錯誤預測比添加錯誤要痛苦得多。

在執行加法之前有條件地進行檢查可以節省時間的唯一情況是,是否可以避免“昂貴的”寫入操作。 例如,類似:

if (var1 != 0)
    someobject.property1 += var1;

如果寫入propert1會很慢,則可以節省時間,特別是如果屬性尚未優化出寫入已經存在的值的時間。 在極少數情況下,您可能會受益於:

if (var1 != 0)
    volatilevar2 += var1;

如果多個處理器都經常重新讀取volatilevar2,而var1通常為零。 可以懷疑的是,在那里進行的比較是否會自然而然地發生,這是令人懷疑的。 稍作偽造的版本:

if (var1 != 0)
    Threading.Interlocked.Add(volatilevar2, var1);

在某些自然發生的情況下可能會有所幫助。

當然,如果加法的目的地是本地temp變量,該變量不會與其他處理器共享,那么節省時間的可能性實際上為零。

除了這樣的事實:比較通常與加法一樣快(因此平均而言,您會有更多的操作),而且在許多體系結構上,如果CPU無法猜測以哪種方式進行分支,則分支成本很高。會,還有代碼的局部性。

現代處理器要盡可能多地保留在處理器或主板上的緩存中。 命中主內存相對較慢,而讀取內存頁面則相對較慢。 有一個從快和小到慢和大的層次結構。 對於性能而言,重要的一件事是嘗試停留在該層次結構的“快速和小”方面。

您的代碼將處於循環中。 如果該循環適合一兩個緩存行,那么您的狀態就很好,因為CPU可以在絕對少的時間內提取指令來執行該循環,而不會將其他內存從緩存中踢出。

因此,在進行微優化時,應嘗試使內部循環包含小的代碼,這通常意味着簡單而簡短。 在您的情況下,當您無法進行比較時,您需要進行三個比較,並進行多個添加,而必須進行兩次添加。 與更簡單的tempAnswer = work1 + work2 + toCarry;相比,此代碼更有可能導致高速緩存未命中tempAnswer = work1 + work2 + toCarry;

最快是一個相對術語。 這是什么平台? 有緩存嗎? 如果它具有緩存,則很可能在可以在單個時鍾周期內執行添加的平台上,因此無需優化添加。 下一個問題是比較,即減法和減法相加,加法時間相同,因此對於大多數平台而言,新舊交易的比較(減法)不會節省任何費用,最終您會發現分支成本,管道刷新等。即使使用ARM平台,您也只能花很少的時間。 對於此類優化,您要做的第一件事是查看編譯器輸出,編譯器選擇什么指令? (假設這是每個編譯此代碼的人都在使用的編譯器以及相同的編譯器選項,等等)。 例如,在一個芯片中,add / sub占用的時鍾多於一個時鍾,或者大量時鍾,xor或and和or運算可能會占用較少的時鍾。 您可以在某些處理器上使用按位運算與零進行比較,從而節省了時鍾。 編譯器是否發現並使用了更快的操作?

作為通用問題的答案,這取決於所處的處理器以及您正在使用或未使用的處理器的幾率。 單行:

tempAnswer = work1 + work2 + toCarry;

是最優化的代碼。 對於大多數處理器或我猜您可能正在使用的處理器,編譯器會將其轉換為兩個或三個指令。

您最大的擔心不是加法或比較或分支或分支預測,最大的擔心是這些變量保存在寄存器中。 如果它們都必須來回移動到堆棧/內存,即使使用緩存,這也會減慢循環速度。 循環中的其他代碼將確定這一點,您可以在代碼中做一些事情以最大程度地減少寄存器的使用,從而希望這些代碼可以基於寄存器。 同樣,反匯編您的代碼以查看編譯器在做什么。

我同意其他評論的總體觀點-“優化”實際上是一種“悲觀”,使代碼難以編寫,閱讀和維護。

此外,“優化”代碼比簡單代碼更大。

示例函數

$ cat yy.c
int optimizable(int work1, int work2, int toCarry)
{
    int tempAnswer;
    if (work1 == 0)
    {
        if (work2 == 0)
            tempAnswer = toCarry;
        else
            tempAnswer = work2 + toCarry; 
    }
    else if (work2 == 0)
        tempAnswer = work1 + toCarry;
    else
        tempAnswer = work1 + work2 + toCarry;

    return tempAnswer;
}
$ cat xx.c
int optimizable(int work1, int work2, int toCarry)
{
    int tempAnswer;
    tempAnswer = work1 + work2 + toCarry;
    return tempAnswer;
}
$

編譯器

$ gcc --version
gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-44)
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

具有不同優化級別的目標文件大小

$ gcc -c yy.c xx.c
$ size xx.o yy.o
   text    data     bss     dec     hex filename
     86       0       0      86      56 xx.o
    134       0       0     134      86 yy.o
$ gcc -O -c yy.c xx.c
$ size xx.o yy.o
   text    data     bss     dec     hex filename
     54       0       0      54      36 xx.o
     71       0       0      71      47 yy.o
$ gcc -O1 -c yy.c xx.c
$ size xx.o yy.o
   text    data     bss     dec     hex filename
     54       0       0      54      36 xx.o
     71       0       0      71      47 yy.o
$ gcc -O2 -c yy.c xx.c
$ size xx.o yy.o
   text    data     bss     dec     hex filename
     54       0       0      54      36 xx.o
     70       0       0      70      46 yy.o
$ gcc -O3 -c yy.c xx.c
$ size xx.o yy.o
   text    data     bss     dec     hex filename
     54       0       0      54      36 xx.o
     70       0       0      70      46 yy.o
$ gcc -O4 -c yy.c xx.c
$ size xx.o yy.o
   text    data     bss     dec     hex filename
     54       0       0      54      36 xx.o
     70       0       0      70      46 yy.o
$

該代碼針對AMD x86-64上的64位RedHat Linux進行了編譯。

這兩個功能攜帶相同的基礎設施包(3個參數,1個本地,1個返回)。 最佳情況下,優化功能最多比未優化功能長16個字節。 將多余的代碼讀取到內存中會降低性能,而執行該代碼所花費的額外時間則是另一個。

這是經典的告誡:“避免早期優化”。

功能真的那么重要嗎? 它被調用了很多次以至於必須對其進行優化嗎?

現在,讓我們看一下喬納森的回答,並思考“技術債務”,即可維護性。 在您的特定環境中思考:一兩年后,有人會看您的代碼,發現它很難理解,或者更糟的是,他/她會誤解它!

最重要的是,比較xx.c和yy.c:哪段代碼更有可能出現錯誤?

祝好運!

暫無
暫無

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

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