**python
**
爲何要學習 python ?可能由於它是「膠水」吧算法
**ruby
**
說出來可能不信,我最近在追劇,《老友記》《生活大爆炸》《黑袍糾察隊》等等,果真心有多大,這個浪的舞臺就有多大,考試的事情要不是有人提醒,已然把它扔到爪哇國了;心中每天帶着木有學習的愧疚感,再雜夾着追劇所帶來的愉悅,啊,人果真是矛盾的生物啊;可是我又很開心,你說這個事氣不氣人吧?多線程
看完上述描述,是否是以爲我又習慣性跑題了?其實這種矛盾感在寫代碼的時候也是經常發生,好比我第一門語言學的是C,可是工做經常用Java;雖然用Java讓人以爲寫起來很爽(面向對象,你懂的,只是處理業務上的邏輯,其餘方面的細節幾乎能夠忽略掉),可是個人心裏是抗拒的;併發
若是沒有對象,那麼就 new 一個出來,看似簡單平直的想法卻帶來了很大的性能上的問題,隨着對象的日益增多,硬件便很難長久的支撐下去,這個事情就會變得很氣人,原本 new 出來一個對象就是爲了方便使用的,結果我仍是得了解它的生老病死,不然生怕一個不當心就把硬件給撐爆掉;爲了解決這種會一不當心撐爆硬件的事情發生,最直白的想法就是少new對象,若是非new不可,那麼就new一個足夠精簡的,因此最後的問題又被轉移到如何設計對象上面去了,,,
因此,寫Java時間久了真的會有一種成就感,你看我就是我那堆代碼的上帝,我掌握着它們的生老病死,,,但是上帝也不是那麼好當的啊,咱也沒那麼多精力去維護啊,因此我就以爲寫Java就是矛盾感特別嚴重的一件事情:爲了省心,我選擇面向對象,可是我選擇了面向對象,卻再也沒省過心,,,oop
從上述吐槽當中,個人大腦又開了一個腦洞,我以爲設計一門語言絕對是個哲學問題,想要知足全部人的需求是不可能,那麼這種不多是不是能夠用某種簡化思惟來替代,好比:須要對象的時候我用Java,須要性能的時候我用C,須要寫的優雅的時候,我就選擇ruby?(maybe or not,who care?)
因此,我如今想選擇 python (hahaha,,,沒想到吧,上面提到的我都不要)
聽說這貨已經被歸入高考考試範疇了,這你敢信?可是,事實就是,,,emmmm,怎麼說好呢?若是第一門語言學的是python,那麼學其餘語言要付出更多的辛苦,並且學python不就是爲了使用更多的語言嗎???(此處手動狗頭,僅爲一家之言)
因此,吐槽完一遍,最後琢磨了一下,仍是要有一門特別熟悉的語言做爲模板,再輔以《編譯原理》等精神食糧以滋養,才能在突飛猛進的互聯網時代生存下去,在不一樣的事物之間尋找共性是一個比較耗時費力的事情,可是也是比較有成就感的事情吧,,,性能
因此,今天仍是再探索一下 計算機底層 吧,,, hia,hia, hia~~~學習
下面是一個實驗,可能裏面的數字不甚準確,可是意思差很少,領會到就好:
注:這個實驗是在 2009 年全國信息學奧林匹克冬令營論文 中看到的,做者未知,可是很感謝這位提供實驗數據的仁兄測試
題目:在一堆數字中,找到其中的最大值;
分析:最樸素的想法就是循環一遍,找出最大值便可,這樣的話這個算法的複雜度主要集中在數字的多少上,設一共有n個數字,則算法複雜度的上限爲 O(n)優化
1.這個時候寫一個for循環解法,令輸入10000000個數字,每隔100個數字計算一次其平均時鐘週期,大概處理一個數字會佔到7~8個時鐘週期,總的測量週期值爲: 75305510
代碼以下:
int get_max(int* a,int l){ int mx=0,i; for(i=0;i<l;i++){ if(a[i]>mx) mx=a[i]; } return mx; }
2.顯然,上面這個思想太樸素了,必定能夠有個縮減效率的辦法,仔細觀察會發現 a[i] 出現了兩次,實際上就是對 a[i] 作了兩次尋址的操做,那麼幹脆用地址如何?
int get_max(int* a,int l){ int mx=0,*ed=a+l; while(a!=ed){ if(*a>mx)mx=*a; a++; } return mx; }
這一段代碼直接讀取 a 的地址,因此理論上效率必定是有所提高的,結果也的確如此,大概處理一個數字會平均佔6~7個始終週期,總的測量週期值爲: 66047005
3.emmmm,難到這就是結束了嗎?好像再怎麼從語言語法的層面觀察都不能再簡化操做了,那應該怎麼辦?思考一下,一共1千萬個數字,上述操做是進行了一遍循環,若是我同時分紅8個線路,同時進行尋找最大值的操做會不會更快一些?
int get_max(int* a,int l){ assert(l%8==0); #define D(x) mx##x=0 int D(0),D(1),D(2),D(3), D(4),D(5),D(6),D(7),*ed=a+l; #define CMP(x) if(*(a+x)>mx##x)mx##x=*(a+x); while(a!=ed){ CMP(0);CMP(1); CMP(2);CMP(3); CMP(4);CMP(5); CMP(6);CMP(7); a+=8; } #define CC(x1,x2) if(mx##x1>mx##x2)mx##x2=mx##x1; CC(1,0);CC(3,2); CC(5,4);CC(7,6); CC(2,0);CC(6,4); CC(4,0); return mx0; }
總的測量週期值爲: 34818706
看起來效果很顯著,比第一個想法的時鐘週期縮短了近一半,可是有個疑問,只不過是單路運行轉成了8路運行而已,而後速度就快了一倍,是否是有些太誇張了?那麼這裏如何解釋呢?
簡單的講,在最初兩個程序中,每次計算新的 mx 都會依賴於上一步的計算結果,相關的計算指令也必須依次運行,而將求值過程分爲多路處理,mx0,mx1 等變量的相關指令之間互相沒有關聯,讓處理器有更大的機會將他們併發。
4.還能夠進行優化嗎?來讓咱們回顧一下高級程序設計語言的誕生的目的,OK,你懂的,爲了方便人類使用實際上是在 彙編語言 的基礎之上作了一些性能上的讓步,高級語言過於依賴內存變量這一律念,而讀寫內存,是處理器最低效的操做之一,因此直接用 彙編語言 編寫 1號 代碼會發生什麼?
int get_max(int* a,int l){ int ret; __asm__ __volatile__ ( "movl $0, %%eax\n\t" ".p2align 4,,15\n" "LP1:\n\t" "cmpl -4(%1,%2,4), %%eax\n\t" "jge ED\n\t" "movl -4(%1,%2,4), %%eax\n" "ED:\n\t" //"loop LP1\n\t" "decl %2\n\t" "jnz LP1\n\t" "movl %%eax, %0\n\t" :"=m"(ret) :"r"(a),"c"(l) :"%eax"); return ret; }
總的測量週期值爲: 21322853
其實思路上與 1號 代碼是相同的,可是效果是顯著的,優化了近72%,打量一下這個程序,核心循環中,有 5 條指令,其中甚至有兩條是條件分支指令,還有兩條須要訪問內存,並且使用了最複雜的 sib 尋址方式。感受起來,平均 2 個時鐘週期,是沒有道理的,其實這主要得益於現代 CPU 各類強大的優化機制:高速數據 cache 使兩次訪問同一內存如同訪問寄存器通常迅速,第一個條件跳轉大部分時間不會成立,而相反第二個跳轉總會成立,這讓 CPU 的分支預測發揮到極致。而強大的亂序執行引擎使得循環中的這些小指令得以以接近雙倍的時間運行。
固然,在上述代碼中有一條 loop 語句被註釋掉了,若是利用 loop 進行循環操做會是怎樣的效果?結果使人大跌眼鏡:平均耗費 56457348 個時鐘週期,整整慢了一倍還多,爲何會這麼慢?
其實這個主要是由於cpu廠商對 指令集 的優化處理的結果,由於工程師們發現,事實上人們所使用的 80%的指令都處於 20%的指令集中,因而設計了RISC(精簡指令集計算機),經過採用一個較小但功能完備的指令集,大大簡化處理器的設計。RISC 中再也不須要微指令的概念,而直接硬件執行指令碼,在一個時鐘週期執行一條指令,性能極高且容易控制。
因此咱們能夠預見到 loop 這種須要解析成多條微指令的複雜指令是不會被cpu的製造商進行優化處理的,因此用這條語句執行命令,天然會慢
5.既然用 彙編語言 重寫一遍1號代碼帶來的優化如此巨大,那麼用 彙編語言 從新2號代碼呢?
int get_max(int* a,int l){ assert(l%2==0); int ret; __asm__ __volatile__ ( "movl $0, %%eax\n\t" "movl $0, %%edx\n\t" ".p2align 4,,15\n" "LP2:\n\t" "cmpl (%1), %%eax\n\t" "jge ED2\n\t" "movl (%1), %%eax\n" "ED2:\n\t" "cmpl 4(%1), %%edx\n\t" "jge ED3\n\t" "movl 4(%1), %%edx\n" "ED3:\n\t" "addl $8, %1\n\t" "subl $2, %2\n\t" "jnz LP2\n\t" "cmpl %%edx, %%eax\n\t" "cmovll %%edx, %%eax\n\t" "movl %%eax, %0\n\t" :"=m"(ret) :"r"(a),"r"(l) :"%eax","%edx"); return ret; }
總的測量週期值爲: 17447544
emmmm,確實是有些優化,可是卻近乎能夠忽略不計,這是爲何呢?
主要是由於彙編語言生成的代碼,代碼已經十分精簡,在每次循環體第一句 mov 指令 cache miss時,後面並無指令能夠提早來執行。
那麼若是我重寫3號代碼呢?(單路變8路)oh,,,god,,,竟然比上面這個還慢,,,爲何?
由於過多的條件跳轉指令也讓處理器吃不消,,,emmmm,因此瞭解硬件的上限很重要,無腦的多線程是會要命的,,,
6.已經作到了這個程度,考察程序各處彷佛都無利可圖了,優化再次陷入了僵局。要想再取得優化,必須再打開思惟才行。這裏要提到的,是所謂的單指令多數據(SIMD)的方法。
由題目可知,循環是不可避免的,上述的優化都是經過語言特性(彙編語言比高級程序設計語言執行快)和 對尋址操做進行優化的,那麼若是我對循環優化呢?上面也提到了,單路執行變成多路執行很容易「翻車」,那麼若是把多路執行變得足夠小如何?測試一下4路執行的效果如何
int get_max(int* a,int l){ assert(l%4==0); assert(sse2); int ret,tmp[4]; __asm__ __volatile__ ( "\txorps %%xmm0, %%xmm0\n" "LP3:\n" "\tmovdqa %%xmm0, %%xmm1\n" "\tpcmpgtd (%1), %%xmm1\n" "\tandps %%xmm1, %%xmm0\n" "\tandnps (%1), %%xmm1\n" "\torps %%xmm1, %%xmm0\n" "\taddl $16, %1\n" "\tsubl $4, %2\n" "\tjnz LP3\n" "\tmovdqu %%xmm0, (%3)\n" "\tmovl (%3), %%eax\n" "\tcmpl 4(%3), %%eax\n" "\tcmovll 4(%3), %%eax\n" "\tcmpl 8(%3), %%eax\n" "\tcmovll 8(%3), %%eax\n" "\tcmpl 12(%3), %%eax\n" "\tcmovll 12(%3), %%eax\n" "\tmovl %%eax, %0\n" :"=m"(ret) :"r"(a),"r"(l),"r"(tmp) :"%eax"); return ret; }
總的測量週期值爲:15898751
雖然跟5號代碼比沒快多少,可是 simd 這個指令是要比5號代碼的指令複雜的許多的,那爲何用複雜指令還變快了?主要是由於 cpu 的廠商對這個指令作了優化處理,,,
是的,答案就是這麼扯淡,越貼近底層就發現越玄學,如何製做一款cpu還真是一個難以回答的問題;可是咱們有如下幾點是能夠肯定的:
1.除法 命令很慢,且難以優化,通常在 高級程序設計語言 層次是儘可能轉換成 乘法
2.既然 除法 命令,那麼 取模 也快不了多少,因此 取模 運算 通常是不會出如今循環裏面的,都是最後執行,對 負數 取模建議這麼寫:
inline int mod(int a){a%=M;if(a<0)a+=M;return a;};
3.減小浮點除法,其實浮點數的除法能夠想成: a÷b 等價於 a×(1÷b)
因此遇到浮點除法轉成 乘法 是值得一試的,幾乎3次乘法效率約等於作一次除法
4.咱們平常使用的是有符號整數,可是在進行除法時操做數經常能夠保證都是非負的,這時咱們應當先將操做數轉換爲無符號類型再作除法:無符號類型的除法比有符號類型進行得更快,因此這樣確實能夠起到優化做用
5.除以2的k次冪,其實就至關於向右移動k位
上述的一些優化處理僅僅是冰山一角,若是再深究下去,估計這篇文章沒半個多月是搞不定的,因此先暫時停在這裏,往後再說,下回再見~