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

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

0x07-C語言效率(上)

0x07-C語言效率(上)

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

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

對于此,我們可以暫且忽略CPU的設計,因為我們在層面上只能考慮如何迎合編譯器的優(yōu)化規(guī)則,而CPU則是語言以及編譯器的事情了。

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

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

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

開始

  1. .

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

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

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

      /*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ù),get_lenmix_cal分別用來得到data_t長度,以及計算數(shù)據(jù)域的總和。

      /*main.c*/  
      int main(void)
      {
          block* block_in_all[ARR_SIZE]  = { NULL };
          int    result_in_all[ARR_SIZE] = { 0 };
          /*
           * 假設生成了許多的`block`類型對象
           * 將許多的`block`放置在一個數(shù)組中,每個元素類型為`block*`
           * 每個block對象中都包含非空的data_t類型的數(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;
      }

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

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

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

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

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

    循環(huán)展開:

      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)展開中的2次循環(huán)展開,同樣還有n次循環(huán)展開。

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

      data* local_data = process->storage;

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

    以及對于外部存儲位置 result 我們在此處也是存在著浪費,同樣我們應該使用一個臨時變量來存儲總和,而不是每次得到結(jié)果便對它進行解引用操作。

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

    在上方我們可以看見循環(huán)展開被稱作2次循環(huán)展開,那么自然可以推斷有n次循環(huán)展開,自然是有的,對于一個n次循環(huán)展開的式子我們有一個簡便的上界確定公式即:

      reality = len - n + 1;

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

      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;
      }

    解釋:循環(huán)展開將元素相加分為兩個部分,第一部分每次加兩個元素,由于如此做會剩余元素沒有加,故在第二部分將剩下的元素都加起來。

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

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

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

    命令式:

      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;
      }

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

    兩個形式的好處前者對于可預測數(shù)據(jù)來說,是一個很好的模型,后者則是中庸之道,什么是可預測不可預測,比如一個數(shù)是負數(shù)還是正數(shù)這就是不可預測的,用前面那個代碼會有很大的懲罰

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

      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];
      }//也可以分成四路并行,每一路存一個。這種做法充分利用了CPU流水線的性能
      for(;i < len;++i)
          local_result_1 += local_data[i];
    
      *result = local_result_1 + local_result_2;

結(jié)束

Tips:

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

其實這是因為你根本就沒有理解C語言工程這個意思,大部分人是這么測試的:

  1. 首先在一個文件夾里創(chuàng)建兩個文件 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();         //編譯通過,可以運行。
          return 0;
      }
  2. 然后編譯運行,發(fā)現(xiàn)可以通過?。?!標準怎么說在其他文件中不可見?而把static.h去掉#include之后發(fā)現(xiàn)報錯test undefined,瞬間初學者就凌亂了。

  3. 好吧,實際上是前輩們以及教材的錯,因為從始至終,所有外界現(xiàn)象都告訴我們C程序是獨立的一個一個文件組成的,但是并沒有告訴我們要先將他們弄成一個工程!此處如果是使用Visual Studio學習C語言的可能會對工程這個概念理解的稍微好一些,雖然不推薦使用 VS 學習C語言。

  4. 你想要實現(xiàn)static函數(shù)僅在本文件可見的效果,請你先補習一下工程這個概念,對于任何可見或者不可見的概念而言都是建立在一個工程內(nèi)而言,而不是像上方的代碼,使用#include來表示,你都#include了,那還有什么可見不可見的當然都可見了。所以一個static函數(shù)可見于不可見是基于一個個工程里的所有C語言源文件而言的。所以你將??匆娗拜厒冞@么回答你的提問:

      /*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();         //報錯,因為test是static函數(shù)。
          return 0;
      }

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

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

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