在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ C/ 0x07-C語(yǔ)言效率(上)
0x0E-單線程備份(下)
0x11-套接字編程-1
0x05-C語(yǔ)言指針:(Volume-1)
0x13-套接字編程-HTTP服務(wù)器(1)
0x0C-開(kāi)始行動(dòng)
C 語(yǔ)言進(jìn)階
第一部分
0x05-C語(yǔ)言指針(Volume-2)
0x08-C語(yǔ)言效率(下)
0x07-C語(yǔ)言效率(上)
0x04 C代碼規(guī)范
0x0F-多線程備份
0x05-C語(yǔ)言變量
第四部分
0x16-套接字編程-HTTP服務(wù)器(4)
0x0D-單線程備份(上)
總結(jié)
0x01-C語(yǔ)言序言
0x15-套接字編程-HTTP服務(wù)器(3)
0x14-套接字編程-HTTP服務(wù)器(2)
0x17-套接字編程-HTTP服務(wù)器(5)
第三部分
我的C語(yǔ)言
0x06-C語(yǔ)言預(yù)處理器
0x09-未曾領(lǐng)略的新風(fēng)景
0x0A-C線程和Glib的視角
第二部分
0x10-網(wǎng)絡(luò)的世界
0x12-套接字編程-2
0x03-C代碼
0x0B-C語(yǔ)言錯(cuò)誤處理

0x07-C語(yǔ)言效率(上)

0x07-C語(yǔ)言效率(上)

大概所有學(xué)習(xí)C語(yǔ)言的初學(xué)者,都被前輩說(shuō)過(guò),C語(yǔ)言是世界上接近最速的編程語(yǔ)言,當(dāng)然這并不是吹牛,也并不是貶低其他語(yǔ)言,誠(chéng)然非C語(yǔ)言能寫(xiě)出高速度的代碼,但是C語(yǔ)言更容易寫(xiě)出高速的程序(高速不代表高效),然而再好的工具,在外行人手中也只能是黯淡沒(méi)落。

對(duì)于現(xiàn)代編譯器,現(xiàn)代CPU而言,我們要盡量迎合CPU的設(shè)計(jì)(比如架構(gòu)和處理指令的方式等),雖然編譯器是為程序員服務(wù),并且在盡它最大的能力來(lái)優(yōu)化程序員寫(xiě)出的代碼,但是畢竟它還沒(méi)有脫離電子的范疇,如果我們的代碼不能讓編譯器理解,編譯器無(wú)法幫我們優(yōu)化代碼,那么我們就無(wú)法寫(xiě)出一個(gè)高速的程序。

對(duì)于此,我們可以暫且忽略CPU的設(shè)計(jì),因?yàn)槲覀冊(cè)趯用嫔现荒芸紤]如何迎合編譯器的優(yōu)化規(guī)則,而CPU則是語(yǔ)言以及編譯器的事情了。

提高程序的速度,就C語(yǔ)言而言可以有這幾種方法:

  • 首先還是要設(shè)計(jì)合理的大綱,正所謂一個(gè)程序最大的性能提升就是它第一次運(yùn)行的時(shí)候
  • 要避免連續(xù)的函數(shù)調(diào)用。
  • 消除不必要的存儲(chǔ)器使用(并非推薦使用register)
  • 使用循環(huán)展開(kāi)技巧,一般編譯器的優(yōu)化選項(xiàng)能自動(dòng)幫你修改代碼成循環(huán)展開(kāi)
  • 對(duì)于一個(gè)操作的核心耗時(shí)部分,通過(guò)重新組合技術(shù)來(lái)提高速度
  • 多采用幾種風(fēng)格的寫(xiě)法,而不是直觀的認(rèn)為,因?yàn)橛?jì)算機(jī)的想法和你是不一樣的
  • 注:隨著編譯器的版本更新,即使不開(kāi)啟優(yōu)化選項(xiàng),自帶的編譯器優(yōu)化依舊能夠?yàn)槲覀兙帉?xiě)的代碼提供一部分優(yōu)化,這便是不使用老版本編譯器的原因,雖然作為一個(gè)程序員不應(yīng)該太依賴(lài)于編譯器,但是我認(rèn)為,時(shí)代在進(jìn)步,信息量正在無(wú)限的膨脹,但是人類(lèi)的大腦以及精力在一個(gè)大時(shí)代內(nèi)是有限的,換句話說(shuō)對(duì)于普通人而言我們的記憶是有限的,我們不應(yīng)該把精力放在前人已經(jīng)做完的事情上,而是要站在巨人的肩膀上向更遠(yuǎn)處眺望,如此我們應(yīng)該充分利用工具來(lái)幫助我們實(shí)現(xiàn)一些既有的功能,而程序員應(yīng)該更 專(zhuān)注于發(fā)現(xiàn)新的思路,以及想法,在圖靈測(cè)試尚未有人打破之前,程序員依賴(lài)編譯器并不是一件錯(cuò)誤的事情。

    對(duì)于當(dāng)下的編譯器,以GCC(GCC不僅僅是一個(gè)編譯器,但這里將它當(dāng)成編譯器的代名詞)為例,-O2是一個(gè)為大眾所接受的優(yōu)化等級(jí),對(duì)于其他編譯器,一般程序員可以選擇使用由Google和Apple聯(lián)合開(kāi)發(fā)的編譯器clang也是一個(gè)很好的選擇, 在-O2的優(yōu)化等級(jí)下,GCC一般情況下能夠自動(dòng)執(zhí)行循環(huán)展開(kāi)優(yōu)化,

開(kāi)始

  1. .

      /*struct.h*/   
      #include <stdio.h>
      typedef struct me{
              int        value;
              struct me* next;
      }data_t;
    
      typedef struct{
              int index;
              data_t* storage;
      }block; 

    為了測(cè)試方便我們首先定義了兩個(gè)結(jié)構(gòu)體,分別是:

    block代表一個(gè)塊,每個(gè)塊都有一個(gè)序號(hào)(int),一個(gè)數(shù)據(jù)域data_t
    data_t代表一個(gè)數(shù)據(jù)域,原型是一個(gè)鏈表,每個(gè)data_t對(duì)象中包含一個(gè)數(shù)據(jù)和一個(gè)指針。

      /*main.c*/
      #include "struct.h"
      #define ARR_SIZE 10
      static inline int get_len(const data_t* data)
      {
          int len = 0;
    
          if(!data)
              fprintf(stderr,"The data in %p is NULL\n",data);
          else
              while(!data->next)
              {
                  ++len;
                  data = data->next;
              }
          return len;
      }
    
      static inline void mix_cal(const block* process, int result[])
      {
          for(int i = 0;i < get_len(process->storage);++i)
          {
              *result += (process->storage)[i];
          }
      }

    此時(shí)我們得到了兩個(gè)測(cè)試函數(shù),get_lenmix_cal分別用來(lái)得到data_t長(zhǎng)度,以及計(jì)算數(shù)據(jù)域的總和。

      /*main.c*/  
      int main(void)
      {
          block* block_in_all[ARR_SIZE]  = { NULL };
          int    result_in_all[ARR_SIZE] = { 0 };
          /*
           * 假設(shè)生成了許多的`block`類(lèi)型對(duì)象
           * 將許多的`block`放置在一個(gè)數(shù)組中,每個(gè)元素類(lèi)型為`block*`
           * 每個(gè)block對(duì)象中都包含非空的data_t類(lèi)型的數(shù)據(jù)域
           */
          for(int i = 0;i < ARR_SIZE;++i)
          {
              mix_cal(block_in_all[i], result_in_all+i);
          }
          for(int i = 0;i < ARR_SIZE;++i)
          {
              printf("The %dth block have the total %d data\n",
                          block_in_all[i]->index, result_in_all[i]);
          }
    
          return 0;
      }

    耐心讀完上述的代碼,它是用來(lái)求和的,求一個(gè)域中的所有元素的和。仔細(xì)分析一下,很容易就能看見(jiàn)一些缺點(diǎn),最大的莫過(guò)于在mix_cal函數(shù)中對(duì)于get_len函數(shù)的調(diào)用,在此處看來(lái)十分明顯,但是我們?cè)诰帉?xiě)程序的時(shí)候是否能夠注意到這個(gè)問(wèn)題呢?
    對(duì)于一些不必要的函數(shù)調(diào)用我們要做的便是將他們提取出來(lái),使用臨時(shí)變量是一個(gè)很好的辦法,因?yàn)樵诰幾g器的幫助下臨時(shí)變量允許的情況下能夠充分的利用CPU的寄存器。之所以是允許的情況下,是因?yàn)榧拇嫫鞯臄?shù)量并不多,而編譯器在寄存器的使用上需要考慮許多的復(fù)雜因素,故并不是每次使用臨時(shí)變量都能加入寄存器。但這并不妨礙我們提升程序的性能。

    在此處,我們應(yīng)該將for循環(huán)中的判斷語(yǔ)句里的get_len函數(shù)提取出來(lái),在外部使用一個(gè)臨時(shí)變量接收結(jié)果,而不是在循環(huán)中一直調(diào)用該函數(shù)。

      int len = get_len(process->storage);
  2. .

    依舊是上方的代碼,我們來(lái)講述一下,循環(huán)展開(kāi)。

    對(duì)于mix_cal函數(shù),我們或者說(shuō)編譯器可以如何提升它的速度呢?我們說(shuō)過(guò)一點(diǎn)的小改變都可能對(duì)一個(gè)程序的最終代碼產(chǎn)生極大的影響,對(duì)此最常用的便是嘗試,前人之路早已鋪好,不需要重復(fù)造輪子了。

    循環(huán)展開(kāi):

      int reality = len - 1, i;
      for(i = 0;i < reality;i+=2)
      {
          *result = *result + (process->storage)[i] 
                            + (process->storage)[i+1];
      }
      for(;i < len;++i)
      {
          *result +=  (process->storage)[i];
      }

    這就是循環(huán)展開(kāi)中的2次循環(huán)展開(kāi),同樣還有n次循環(huán)展開(kāi)。

    同樣,在剛才提到過(guò)寄存器的使用以及減少不必要的開(kāi)銷(xiāo),在此程序中對(duì)于(process->storage)[i]這樣的存儲(chǔ)器位置解引用太過(guò)浪費(fèi),我們總是將其優(yōu)化成本低臨時(shí)變量的使用

      data* local_data = process->storage;

    這將為程序帶來(lái)十分可觀的節(jié)約,雖然這些工作在編譯器的優(yōu)化中都能包括,但是一旦我們的代碼難以被編譯器所理解(雖然編譯器的升級(jí)最大的目的就是提升優(yōu)化效果),那么我們很可能得到一個(gè)性能不夠可觀的程序。所以當(dāng)我們并不是特別緊湊的時(shí)候,可以將這些工作當(dāng)成我們的本分來(lái)做,而不是交給編譯器來(lái)做。

    以及對(duì)于外部存儲(chǔ)位置 result 我們?cè)诖颂幰彩谴嬖谥速M(fèi),同樣我們應(yīng)該使用一個(gè)臨時(shí)變量來(lái)存儲(chǔ)總和,而不是每次得到結(jié)果便對(duì)它進(jìn)行解引用操作。

      int local_result = 0;
      /*...*/
      local_result = local_result + local_data[i] + local_data[i+1];
      /*...*/
      *result = local_result;

    在上方我們可以看見(jiàn)循環(huán)展開(kāi)被稱(chēng)作2次循環(huán)展開(kāi),那么自然可以推斷有n次循環(huán)展開(kāi),自然是有的,對(duì)于一個(gè)n次循環(huán)展開(kāi)的式子我們有一個(gè)簡(jiǎn)便的上界確定公式即:

      reality = len - n + 1;

    至于展開(kāi)幾次最好,依然是視環(huán)境而定。 故最終的版本應(yīng)該為:

      static inline void mix_cal(const block* process, int result[])
      {
          int local_result = 0;
          int len = get_len(process->storage);
          int reality = len - 1, i;
          data* local_data = process->storage;
    
          for(i = 0;i < reality;i+=2)
              local_result += local_data[i] + local_data[i+1];
          for(;i < len;++i)
              local_result += local_data[i];
    
          *result = local_result;
      }

    解釋?zhuān)貉h(huán)展開(kāi)將元素相加分為兩個(gè)部分,第一部分每次加兩個(gè)元素,由于如此做會(huì)剩余元素沒(méi)有加,故在第二部分將剩下的元素都加起來(lái)。

  3. . 還有一種叫做重新組合的技巧,即為讓一個(gè)表達(dá)式中的運(yùn)算數(shù)自由組合,組合出最快速的一種,但是這種方法未曾試驗(yàn)過(guò)。故不提及。

  4. . 對(duì)于條件分支預(yù)測(cè)錯(cuò)誤造成的時(shí)間損耗,稱(chēng)之為懲罰,最通俗的說(shuō)法,就是當(dāng)你編寫(xiě)的代碼中含有條件分支的時(shí)候,處理器會(huì)選擇去預(yù)判某一個(gè)分支是此次正確的支路,這樣可以避免修改任何實(shí)際的寄存器和存儲(chǔ)器,一直到確定了實(shí)際結(jié)果,要是不對(duì),那就慘了,這段時(shí)間做的事情都白費(fèi)了。但是也不必過(guò)分的關(guān)心這種條件分支的預(yù)測(cè),這也是我放在最后說(shuō)的意義所在。

    這里有兩種較為客觀的方法,一種被稱(chēng)為命令式,一種被稱(chēng)為功能式

    命令式:

      for(int i = 0;i < n;++i)
      {
          if(a[i] > b[i]){
              int temp = a[i];
              a[i] = b[i];
              b[i] = temp;
          }
      }

    功能式:

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

    很清晰的一個(gè)例子,明顯看出來(lái),前者對(duì)于不同情況所作的程序步數(shù)明顯不同,而后者無(wú)論什么情況都是相同的程序步。

    兩個(gè)形式的好處前者對(duì)于可預(yù)測(cè)數(shù)據(jù)來(lái)說(shuō),是一個(gè)很好的模型,后者則是中庸之道,什么是可預(yù)測(cè)不可預(yù)測(cè),比如一個(gè)數(shù)是負(fù)數(shù)還是正數(shù)這就是不可預(yù)測(cè)的,用前面那個(gè)代碼會(huì)有很大的懲罰。

  5. . 多路并行的技巧也是一個(gè)很重要的思路,可能在很多人眼中看來(lái),兩條語(yǔ)句依次寫(xiě)出和合并的效果一定是一樣。但是多路并行有一個(gè)缺點(diǎn)就是對(duì)寄存器的數(shù)量有所要求,當(dāng)寄存器不夠時(shí)(稱(chēng)為溢出),性能不升反降。同樣是對(duì)于循環(huán)展開(kāi),此次使用四次循環(huán)展開(kāi)二路并行

      for(i = 0;i < reality;i+=4){
          local_result_1 += local_data[i] + local_data[i+1];
          local_result_2 += local_data[i+2] + local_data[i+3];
      }//也可以分成四路并行,每一路存一個(gè)。這種做法充分利用了CPU流水線的性能
      for(;i < len;++i)
          local_result_1 += local_data[i];
    
      *result = local_result_1 + local_result_2;

結(jié)束

Tips:

上文中寫(xiě)到的函數(shù)大都帶有static inline關(guān)鍵字,這是何意?首先我們要確定一件事情,對(duì)于非工程的單文件而言,static函數(shù)并沒(méi)有什么意義(意義指的是對(duì)于可見(jiàn)性而言,并非說(shuō)它一無(wú)是處),許多人對(duì)于static函數(shù)感到茫然的原因在于:我明明將一個(gè)函數(shù)聲明定義成static類(lèi)型了,但是我還是可以在別的文件中訪問(wèn)到啊!

其實(shí)這是因?yàn)槟愀揪蜎](méi)有理解C語(yǔ)言工程這個(gè)意思,大部分人是這么測(cè)試的:

  1. 首先在一個(gè)文件夾里創(chuàng)建兩個(gè)文件 test_static.cstatic.h:

      /*static.h*/
      #ifndef STATIC_H
      #define STATIC_H
      static void test(void);
    
      static void test(void);
      {
          printf("Hello World!\n");
      }
      #endif

    ...

      /*test_static.c*/
      #include <stdio.h>
      #include "static.h"
    
      void test(void);
      int main(void)
      {
          test();         //編譯通過(guò),可以運(yùn)行。
          return 0;
      }
  2. 然后編譯運(yùn)行,發(fā)現(xiàn)可以通過(guò)?。。?biāo)準(zhǔn)怎么說(shuō)在其他文件中不可見(jiàn)?而把static.h去掉#include之后發(fā)現(xiàn)報(bào)錯(cuò)test undefined,瞬間初學(xué)者就凌亂了。

  3. 好吧,實(shí)際上是前輩們以及教材的錯(cuò),因?yàn)閺氖贾两K,所有外界現(xiàn)象都告訴我們C程序是獨(dú)立的一個(gè)一個(gè)文件組成的,但是并沒(méi)有告訴我們要先將他們弄成一個(gè)工程!此處如果是使用Visual Studio學(xué)習(xí)C語(yǔ)言的可能會(huì)對(duì)工程這個(gè)概念理解的稍微好一些,雖然不推薦使用 VS 學(xué)習(xí)C語(yǔ)言。

  4. 你想要實(shí)現(xiàn)static函數(shù)僅在本文件可見(jiàn)的效果,請(qǐng)你先補(bǔ)習(xí)一下工程這個(gè)概念,對(duì)于任何可見(jiàn)或者不可見(jiàn)的概念而言都是建立在一個(gè)工程內(nèi)而言,而不是像上方的代碼,使用#include來(lái)表示,你都#include了,那還有什么可見(jiàn)不可見(jiàn)的當(dāng)然都可見(jiàn)了。所以一個(gè)static函數(shù)可見(jiàn)于不可見(jiàn)是基于一個(gè)個(gè)工程里的所有C語(yǔ)言源文件而言的。所以你將常看見(jiàn)前輩們這么回答你的提問(wèn):

      /*static.h*/
      #ifndef STATIC_H
      #define STATIC_H
      static void test(void);
    
      static void test(void);
      {
          printf("Hello World!\n");
      }
      #endif

    ...

      /*test_static.c*/
      #include <stdio.h>
    
      void test(void);
      int main(void)
      {
          test();         //報(bào)錯(cuò),因?yàn)閠est是static函數(shù)。
          return 0;
      }

    發(fā)現(xiàn)了嗎?在上方代碼中,少了一行#include "static.h"但是這個(gè)代碼依舊可行,因?yàn)檫@兩個(gè)文件是建立在同一個(gè)工程里的,而不是在一個(gè)文件夾中隨意新建兩個(gè)源文件這么簡(jiǎn)單,你可以使用各個(gè)IDE的工程功能來(lái)進(jìn)行測(cè)試。

回到正題,在這里稍微提一下static對(duì)函數(shù)的某些作用,它可以讓函數(shù)放在一個(gè)靜態(tài)的空間中,而不是棧里,這是的它的調(diào)用更加快速,經(jīng)常與inline關(guān)鍵字一起使用,為的就是讓函數(shù)更加快。但是有利有弊,可以自己權(quán)衡一下。

參考:深入理解計(jì)算機(jī)系統(tǒng)--Randal E.Bryant / David O'Hallaro