當前位置:首頁 > 公眾號精選 > 嵌入式大雜燴
[導讀]我們寫程序的目的就是使它在任何情況下都可以穩(wěn)定工作。

我們寫程序的目的就是使它在任何情況下都可以穩(wěn)定工作。一個運行的很快但是結果錯誤的程序并沒有任何用處。在程序開發(fā)和優(yōu)化的過程中,我們必須考慮代碼使用的方式,以及影響它的關鍵因素。通常,我們必須在程序的簡潔性與它的運行速度之間做出權衡。今天我們就來聊一聊如何優(yōu)化程序的性能。

1. 減小程序計算量

1.1 示例代碼

for (i = 0; i < n; i++) { int ni = n*i; for (j = 0; j < n; j++) a[ni + j] = b[j]; }

1.2 分析代碼

代碼如上所示,外循環(huán)每執(zhí)行一次,我們要進行一次乘法計算。i = 0,ni = 0;i = 1,ni = n;i = 2,ni = 2n。因此,我們可以把乘法換成加法,以n為步長,這樣就減小了外循環(huán)的代碼量。

1.3 改進代碼

int ni = 0; for (i = 0; i < n; i++) { for (j = 0; j < n; j++) a[ni + j] = b[j]; ni += n; //乘法改加法 }

計算機中乘法指令要比加法指令慢得多。

2. 提取代碼中的公共部分

2.1 示例代碼

想象一下,我們有一個圖像,我們把圖像表示為二維數組,數組元素代表像素點。我們想要得到給定像素的東、南、西、北四個鄰居的總和。并求他們的平均值或他們的和。代碼如下所示。

up =    val[(i-1)*n + j  ];
down =  val[(i+1)*n + j  ];
left =  val[i*n     + j-1];
right = val[i*n     + j+1];
sum = up + down + left + right;

2.2 分析代碼

將以上代碼編譯后得到匯編代碼如下所示,注意下3,4,5行,有三個乘以n的乘法運算。我們把上面的up和down展開后會發(fā)現(xiàn)四格表達式中都有i*n + j。因此,可以提取出公共部分,再通過加減運算分別得出up、down等的值。

leaq   1(%rsi), %rax # i+1 leaq   -1(%rsi), %r8 # i-1 imulq  %rcx, %rsi # i*n imulq  %rcx, %rax # (i+1)*n imulq  %rcx, %r8 # (i-1)*n addq   %rdx, %rsi # i*n+j addq   %rdx, %rax # (i+1)*n+j addq   %rdx, %r8 # (i-1)*n+j 

2.3 改進代碼

long inj = i*n + j;
up =    val[inj - n];
down =  val[inj + n];
left =  val[inj - 1];
right = val[inj + 1];
sum = up + down + left + right;

改進后的代碼的匯編如下所示。編譯后只有一個乘法。減少了6個時鐘周期(一個乘法周期大約為3個時鐘周期)。

imulq %rcx, %rsi # i*n addq %rdx, %rsi # i*n+j movq %rsi, %rax # i*n+j subq %rcx, %rax # i*n+j-n leaq (%rsi,%rcx), %rcx # i*n+j+n ...

對于GCC編譯器來說,編譯器可以根據不同的優(yōu)化等級,有不同的優(yōu)化方式,會自動完成以上的優(yōu)化操作。下面我們介紹下,那些必須是我們要手動優(yōu)化的。

3. 消除循環(huán)中低效代碼

3.1 示例代碼

程序看起來沒什么問題,一個很平常的大小寫轉換的代碼,但是為什么隨著字符串輸入長度的變長,代碼的執(zhí)行時間會呈指數式增長呢?

void lower1(char *s) { size_t i; for (i = 0; i < strlen(s); i++) if (s[i] >= 'A' && s[i] <= 'Z')
      s[i] -= ('A' - 'a');
}

3.2 分析代碼

那么我們就測試下代碼,輸入一系列字符串。

lower1代碼性能測試

當輸入字符串長度低于100000時,程序運行時間差別不大。但是,隨著字符串長度的增加,程序的運行時間呈指數時增長。

我們把代碼轉換成goto形式看下。

void lower1(char *s) { size_t i = 0; if (i >= strlen(s)) goto done;
 loop: if (s[i] >= 'A' && s[i] <= 'Z')
       s[i] -= ('A' - 'a');
   i++; if (i < strlen(s)) goto loop;
 done:
}

以上代碼分為初始化(第3行),測試(第4行),更新(第9,10行)三部分。初始化只會執(zhí)行一次。但是測試和更新每次都會執(zhí)行。每進行一次循環(huán),都會對strlen調用一次。

下面我們看下strlen函數的源碼是如何計算字符串長度的。

size_t strlen(const char *s) { size_t length = 0; while (*s != '\0') {
 s++; 
 length++;
    } return length;
}

strlen函數計算字符串長度的原理為:遍歷字符串,直到遇到‘\0’才會停止。因此,strlen函數的時間復雜度為O(N)。lower1中,對于長度為N的字符串來說,strlen 的調用次數為N,N-1,N-2 ... 1。對于一個線性時間的函數調用N次,其時間復雜度接近于O(N2)。

3.3 改進代碼

對于循環(huán)中出現(xiàn)的這種冗余調用,我們可以將其移動到循環(huán)外。將計算結果用于循環(huán)中。改進后的代碼如下所示。

void lower2(char *s) { size_t i; size_t len = strlen(s); for (i = 0; i < len; i++) if (s[i] >= 'A' && s[i] <= 'Z')
      s[i] -= ('A' - 'a');
}

將兩個函數對比下,如下圖所示。lower2函數的執(zhí)行時間得到明顯提升。

lower1和lower2代碼效率

4. 消除不必要的內存引用

4.1 示例代碼

以下代碼作用為,計算a數組中每一行所有元素的和存在b[i]中。

void sum_rows1(double *a, double *b, long n) { long i, j; for (i = 0; i < n; i++) { b[i] = 0; for (j = 0; j < n; j++) b[i] += a[i*n + j]; } }

4.2 分析代碼

匯編代碼如下所示。

# sum_rows1 inner loop .L4:
        movsd   (%rsi,%rax,8), %xmm0 # 從內存中讀取某個值放到%xmm0 addsd   (%rdi), %xmm0 # %xmm0 加上某個值 movsd   %xmm0, (%rsi,%rax,8) # %xmm0 的值寫回內存,其實就是b[i] addq $8, %rdi
        cmpq    %rcx, %rdi
        jne     .L4

這意味著每次循環(huán)都需要從內存中讀取b[i],然后再把b[i]寫回內存 。b[i] +=  b[i] + a[i*n + j]; 其實每次循環(huán)開始的時候,b[i]就是上一次的值。為什么每次都要從內存中讀取出來再寫回呢?

4.3 改進代碼

/* Sum rows is of n X n matrix a
   and store in vector b  */ void sum_rows2(double *a, double *b, long n) { long i, j; for (i = 0; i < n; i++) { double val = 0; for (j = 0; j < n; j++) val += a[i*n + j]; b[i] = val; } }

匯編如下所示。

# sum_rows2 inner loop .L10:
        addsd   (%rdi), %xmm0 # FP load + add addq $8, %rdi
        cmpq    %rax, %rdi
        jne     .L10

改進后的代碼引入了臨時變量來保存中間結果,只有在最后的值計算出來時,才將結果存放到數組或全局變量中。

5.  減小不必要的調用

5.1 示例代碼

為了方便舉例,我們定義一個包含數組和數組長度的結構體,主要是為了防止數組訪問越界,data_t可以是int,long等類型。具體如下所示。

typedef struct{ size_t len; data_t *data;  
} vec;

vec向量示意圖

get_vec_element函數的作用是遍歷data數組中元素并存儲在val中。

int get_vec_element (*vec v, size_t idx, data_t *val) { if (idx >= v->len) return 0;
 *val = v->data[idx]; return 1;
}

我們將以以下代碼為例開始一步步優(yōu)化程序。

void combine1(vec_ptr v, data_t *dest) { long int i;
    *dest = NULL; for (i = 0; i < vec_length(v); i++) { data_t val;
 get_vec_element(v, i, &val);
 *dest = *dest * val;
    }
}

5.2 分析代碼

get_vec_element函數的作用是獲取下一個元素,在get_vec_element函數中,每次循環(huán)都要與v->len作比較,防止越界。進行邊界檢查是個好習慣,但是每次都進行就會造成效率降低。

5.3 改進代碼

我們可以把求向量長度的代碼移到循環(huán)體外,同時抽象數據類型增加一個函數get_vec_start。這個函數返回數組的起始地址。這樣在循環(huán)體中就沒有了函數調用,而是直接訪問數組。

data_t *get_vec_start(vec_ptr v) { return v-data;
} void combine2 (vec_ptr v, data_t *dest) { long i; long length  = vec_length(v); data_t *data = get_vec_start(v);
 *dest = NULL; for (i=0;i < length;i++) { *dest = *dest * data[i]; } }

6. 循環(huán)展開

6.1 示例代碼

我們在combine2的代碼上進行改進。

6.2 分析代碼

循環(huán)展開是通過增加每次迭代計算的元素的數量,減少循環(huán)的迭代次數。

6.3 改進代碼

void combine3(vec_ptr v, data_t *dest) { long i; long length = vec_length(v); long limit = length-1; data_t *data = get_vec_start(v); data_t acc = NULL; /* 一次循環(huán)處理兩個元素 */ for (i = 0; i < limit; i+=2) {
    acc = (acc * data[i]) * data[i+1];
    } /*     完成剩余數組元素的計算    */ for (; i < length; i++) { acc = acc * data[i]; } *dest = acc; }

在改進后的代碼中,第一個循環(huán)每次處理數組的兩個元素。也就是每次迭代,循環(huán)索引i加2,在一次迭代中,對數組元素i和i+1使用合并運算。一般我們稱這種為2×1循環(huán)展開,這種變換能減小循環(huán)開銷的影響。

注意訪問不要越界,正確設置limit,n個元素,一般設置界限n-1

7. 累計變量,多路并行

7.1 示例代碼

我們在combine3的代碼上進行改進。

7.2 分析代碼

對于一個可結合和可交換的合并運算來說,比如說整數加法或乘法,我們可以通過將一組合并運算分割成兩個或更多的部分,并在最后合并結果來提高性能。

特別注意:不要輕易對浮點數進行結合。浮點數的編碼格式和其他整型數等都不一樣。

7.3 改進代碼

void combine4(vec_ptr v, data_t *dest) { long i; long length = vec_length(v); long limit = length-1; data_t *data = get_vec_start(v); data_t acc0 = 0; data_t acc1 = 0; /* 循環(huán)展開,并維護兩個累計變量 */ for (i = 0; i < limit; i+=2) {
    acc0 = acc0 * data[i];
    acc1 = acc1 * data[i+1];
    } /*     完成剩余數組元素的計算    */ for (; i < length; i++) { acc0 = acc0 * data[i]; } *dest = acc0 * acc1; }

上述代碼用了兩次循環(huán)展開,以使每次迭代合并更多的元素,也使用了兩路并行,將索引值為偶數的元素累積在變量acc0中,而索引值為奇數的元素累積在變量acc1中。因此,我們將其稱為”2×2循環(huán)展開”。運用2×2循環(huán)展開。通過維護多個累積變量,這種方法利用了多個功能單元以及它們的流水線能力

8. 重新結合變換

8.1 示例代碼

我們在combine3的代碼上進行改進。

8.2 分析代碼

到這里其實代碼的性能已經基本接近極限了,就算做再多的循環(huán)展開性能提升已經不明顯了。我們需要換個思路,注意下combine3代碼中第12行的代碼,我們可以改變下向量元素合并的順序(浮點數不適用)。重新結合前combine3代碼的關鍵路徑如下圖所示。

combine3代碼的關鍵路徑

8.3 改進代碼

void combine7(vec_ptr v, data_t *dest) { long i; long length = vec_length(v); long limit = length-1; data_t *data = get_vec_start(v); data_t acc = IDENT; /* Combine 2 elements at a time */ for (i = 0; i < limit; i+=2) {
   acc = acc OP (data[i] OP data[i+1]);
    } /* Finish any remaining elements */ for (; i < length; i++) { acc = acc OP data[i]; } *dest = acc; }

重新結合變換能夠減少計算中關鍵路徑上操作的數量,這種方法增加了可以并行執(zhí)行的操作數量了,更好地利用功能單元的流水線能力得到更好的性能。重新結合后關鍵路徑如下所示。

combine3重新結合后關鍵路徑

9 條件傳送風格的代碼

9.1 示例代碼

void minmax1(long a[],long b[],long n){ long i; for(i = 0;i,n;i++){ if(a[i]>b[i]){ long t = a[i];
            a[i] = b[i];
            b[i] = t;
        }
   }
}

9.2 分析代碼

現(xiàn)代處理器的流水線性能使得處理器的工作遠遠超前于當前正在執(zhí)行的指令。處理器中的分支預測在遇到比較指令時會進行預測下一步跳轉到哪里。如果預測錯誤,就要重新回到分支跳轉的原地。分支預測錯誤會嚴重影響程序的執(zhí)行效率。因此,我們應該編寫讓處理器預測準確率提高的代碼,即使用條件傳送指令。我們用條件操作來計算值,然后用這些值來更新程序狀態(tài),具體如改進后的代碼所示。

9.3 改進代碼

void minmax2(long a[],long b[],long n){ long i; for(i = 0;i,n;i++){ long min = a[i] < b[i] ? a[i]:b[i]; long max = a[i] < b[i] ? b[i]:a[i]; a[i] = min; b[i] = max; } }

在原代碼的第4行中,需要對a[i]和b[i]進行比較,再進行下一步操作,這樣的后果是每次都要進行預測。改進后的代碼實現(xiàn)這個函數是計算每個位置i的最大值和最小值,然后將這些值分別賦給a[i]和b[i],而不是進行分支預測。

10. 總結

我們介紹了幾種提高代碼效率的技巧,有些是編譯器可以自動優(yōu)化的,有些是需要我們自己實現(xiàn)的?,F(xiàn)總結如下。

  1. 消除連續(xù)的函數調用。在可能時,將計算移到循環(huán)外??紤]有選擇地妥協(xié)程序的模塊性以獲得更大的效率。

  2. 消除不必要的內存引用。引入臨時變量來保存中間結果。只有在最后的值計算出來時,才將結果存放到數組或全局變量中。

  3. 展開循環(huán),降低開銷,并且使得進一步的優(yōu)化成為可能。

  4. 通過使用例如多個累積變量和重新結合等技術,找到方法 提高指令級并行。

  5. 用功能性的風格重寫條件操作,使得編譯采用條件數據傳送。


免責聲明:本文內容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關機構授權發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內容真實性等。需要轉載請聯(lián)系該專欄作者,如若文章內容侵犯您的權益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或將催生出更大的獨角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數字化轉型技術解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關鍵字: AWS AN BSP 數字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術公司SODA.Auto推出其旗艦產品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關鍵字: 汽車 人工智能 智能驅動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務中斷的風險,如企業(yè)系統(tǒng)復雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務連續(xù)性,提升韌性,成...

關鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據媒體報道,騰訊和網易近期正在縮減他們對日本游戲市場的投資。

關鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數據產業(yè)博覽會開幕式在貴陽舉行,華為董事、質量流程IT總裁陶景文發(fā)表了演講。

關鍵字: 華為 12nm EDA 半導體

8月28日消息,在2024中國國際大數據產業(yè)博覽會上,華為常務董事、華為云CEO張平安發(fā)表演講稱,數字世界的話語權最終是由生態(tài)的繁榮決定的。

關鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應對環(huán)境變化,經營業(yè)績穩(wěn)中有升 落實提質增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務引領增長 以科技創(chuàng)新為引領,提升企業(yè)核心競爭力 堅持高質量發(fā)展策略,塑強核心競爭優(yōu)勢...

關鍵字: 通信 BSP 電信運營商 數字經濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術學會聯(lián)合牽頭組建的NVI技術創(chuàng)新聯(lián)盟在BIRTV2024超高清全產業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術創(chuàng)新聯(lián)...

關鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關鍵字: BSP 信息技術
關閉
關閉