Ⅰ C/C++最底層是怎麼實現的
許多同學可能在學習C++的時候,都會感到一定的困惑,繼承到底是怎樣分配空間的,多態到底是如何完成的,許許多多的問題,必須挖掘到C++底層處理機制,才能搞明白。有許多C程序員也並不認同C++,他們認為C++龐大又遲緩,其更重要的原因是,他們認為「C++是在你的背後做事情」。的確,C++編譯器背著程序員做了太多的事情,所以讓很多不了解其底層機制的人感到困惑。想成為一個優秀的程序員,那麼這樣的困惑就不應該存在,只有了解了底層實現模型,才能寫出效率較高的代碼,自信心也比較高。
我們先從一個簡單但有趣的例子談起。有如下的4個類:
class X {};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y, public Z {};
上面的4個類中,沒有任何一個類里含明顯的數據,之間只是表示了繼承關系,那麼如果我們用sizeof 來測量它們的大小,將會得到什麼結果呢?
你可能會認為,既然沒有任何數據和方法,大小當然為0,而結果肯定會出乎你的意料,即使是class X 的大小也不為0。
在不同的編譯器上,將會得到不同的結果,而在我們現在最常用的VC++編譯器上,將得到如下的結果:
sizeof X 的結果是 1 。
sizeof Y 的結果是 4 。
sizeof Z 的結果是 4 。
sizeof A 的結果是 8 。
驚訝嗎?那麼為什麼會得到這樣的結果?讓我們一個一個來分析。
對於一個空的class,事實上並不是空的,它有一個隱晦的1 byte ,那是被編譯器安插進去的一個char 。這使得這個class 的兩個對象得以在內存中配置獨一無二的地址,這就是為什麼 sizeof X 的結果是 1。
那麼Y和Z呢,怎麼會佔用 4 byte ?其實它們的大小受到三個因素的影響:
1. 語言本身所造成的額外負擔:當語言支持多態時,就會導致一些額外負擔。在派生類中,這個額外負擔表現在一個指針上,它是用來指向一個被稱作「虛函數列表」的表格。而在VC++編譯器上,指針的大小正好是 4 byte 。
2. 編譯器對於特殊情況所提供的優化處理:在class Y 和 class Z 中,也將帶上它們因為繼承class X 而帶來的 1 byte ,傳統上它被放在派生類的固定部分的尾端。而某些編譯器(正如我們現在所討論的VC++編譯器)會對空的基類提供特殊的處理,在這個策略下,一個空的基類被視為派生類對象最開頭的一部分,也就是說它並沒有花費任何額外空間(因為既然有了成員,就不需要原本為了空類而安插一個char了),這樣也就節省了這 1 byte 的空間。事實上,如果某個編譯器沒有提供這種優化處理,你將發現class Y 和 class Z 的大小將是8 byte ,而不僅僅是5 byte 了,原因正如下面第3點所講。
3. 「對齊」(Alignment)機制:在大多數機器上,群聚的結構體大小都會受到alignment的限制,使它們能夠更有效率地在內存中被存取。Alignment 就是將數值調整到某數的整數倍。在32位計算機上,通常alignment 為 4 byte(32位),以使匯流排達到最大的「吞吐量」,在這種情況下,如上面所說,如果 class Y 和class Z 的大小為 5 byte ,那麼它們必須填補 3 byte ,最終得到的結果將是 8 byte 。是不是開始感謝VC++編譯器幸好有這樣的優化機制,使得我們節約了不少內存空間。
最後,我們再來看看 class A ,它的大小為什麼是 8 byte ?顯而易見,它繼承了class Y 和class Z ,那麼它的大小直接就把 class Y 和class Z 的大小加起來就夠了。真有這么簡單嗎?實際上這只是一個巧合而已,這是因為之前編譯器的優化機制掩蓋這里的一些事實。對於沒有優化的 class Y 和class Z 來說,他們的大小都是8 byte ,那麼繼承了它們兩個的 class A 將是多大呢?16 byte?如果你有這樣的編譯器試一下的話,你會發現答案是12 byte 。怎麼會是12 byte 呢?記住,一個虛擬繼承的基類只會在派生類中存在一份實體,不管它在 class 繼承體系中出現了多少次!class A的大小由下面幾部分決定:
l 被大家共享的唯一一個 class X的實體,大小為1 byte。
l 基類class Y 的大小,減去因虛擬繼承的基類class X而配置的大小,也就是4 byte 。基類class Z的演算法相同,它們加起來就是8 byte 。
l class A自己的大小,0 byte 。
l class A 的alignment的大小(如果有的話)。前述三項的總和是9 byte ,那麼調整到4 byte的整數倍,也就是12 byte 。
我們前面討論的VC++編譯器得出的結果之所以是8 byte ,是因為 class X 實體的那1 byte被拿掉了,於是額外的3 byte也同樣不必了,因此就直接把class Y 和class Z的大小加起來,得到8 byte 。
這個例子看懂了嗎?是不是對C++的底層機制開始感興趣了?那麼我們再來舉一個同樣有趣的例子。
有這樣一個類:
class A {
private:
int a;
char b;
char c;
char d;
};
它的大小是多少呢?
如果你有記得我之前提到的alignment機制的話,你應該會猜到它的大小是8 byte 。的確如此,int a佔用4 byte ,char b , char c 和char d各佔1 byte ,加起來是7 byte ,再加上alignment額外帶來的1 byte ,總共是8 byte 。
瞧,就是這么簡單,那麼現在我們把裡面的成員變數換換位置,如下:
class A {
private:
char d;
int a;
char b;
char c;
};
我們將char d拿到第一個位子,放在int a之前。那麼現在你能告訴我class A的大小是多少呢?你肯定不會再猜8 byte了,因為你會覺得這與上面似乎有些不同,但你不能肯定到底是多大。不敢確定的時候就去試試吧,原來是12 byte ,這又是怎麼回事呢?同樣的類,只是改變了成員變數的位子,怎麼就會多出4 byte的存儲空間?其實這一切又是由變數的存儲規則造成的。對於一個類來說,它裡面的成員變數(這里單指非靜態的成員變數)是按聲明的順序存儲在內存空間中的。在第一種的情況中,它們緊緊的排列在一起,除了由於alignment所浪費的1 byte空間外,它們幾乎用了最小的存儲空間;而在第二種情況中,它們則不是排列得那麼緊密了,錯誤就在於char d ,它一個人就佔用了4 byte 。為什麼它會佔用4 byte呢,其實責任也不全在它,後面的int a也有不可推卸的責任。Int 型數據在VC++編譯器中正好是佔用4 byte的,等於一個alignment量,而這4 byte一定是密不可分的。當char d佔用了1 byte後,其後空出了3 byte(對於一個alignment量來說),而一個int型數據不能被拆成3 byte +1byte來存儲,那樣編譯器將無法識別,因此int a只有向後推到下一個alignment的開始,這樣char d就獨佔了4 byte ,中間有3 byte浪費掉了。而後面的char b和char c依舊緊密排列,最後又由於alignment調整2 byte ,整個類的大小就變為了12 byte 。
看了這個例子,是不是該反省以前隨意定義成員變數了?如果你要定義一個含3個int型數據和4個char型數據的類,本來最優化的方法只需要16 byte ,而你卻隨意的定義成如下的樣子:
class F{
private:
char c1;
int i1;
char c2;
int i2;
char c3;
int i3;
char c4;
};
看看結果是什麼,這個類竟然要佔據28 byte的空間,比剛才整整大了12 byte!
再來看看繼承的時候,成員變數是怎樣存放的。我們將第2個例子中的class A 改成三層的繼承模式,或許我們在做項目中,真的會遇到這樣的情況。
class A1{
private:
int a;
char b;
};
class A2: public A1{
private:
char c;
};
class A3:public A2{
private:
char d;
};
現在我們來預測一下class A3 的大小,是8 byte嗎?不,結果竟是16 byte ,竟然整整多了1倍。這是為什麼呢?按照成員變數的排列順序,int a,char b,char c,char d應該緊密的排列在一起,8 byte沒錯。但事實並非如此,這些都是因為繼承而造成的。知道「在繼承關系中,基類子對象在派生類中會保持原樣性」嗎?或許這樣專業的一句話,你並不能明白是什麼意思,那麼聽我下面的分析。在為派生類分配內存空間的時候,都是先為基類分配一塊內存空間,而所謂的「原樣性」是指基類原本在內存空間中是什麼樣子,那麼它在派生類里分配的時候就是什麼樣子。拿這個例子來說,class A1占據了8 byte的空間,其中int a佔4 byte ,char b佔1 byte ,因alignment而填補3 byte 。對於class A1來說,占據8 byte空間沒什麼好抱怨的,但是class A2呢?輕率的程序員會認為,class A2隻在class A1的基礎上增加了唯一一個char c ,那麼它應該會和char b綁在一起,佔用原本用來填補空間的1 byte ,於是class A2的大小是8 byte,其中2 byte用於填補空間。然而事實上,char c是被放在填補空間所用的3 byte之後,因為在class A2中分配的class A1應該完全保持原樣,於是class A2的大小變成12 byte ,而不是8 byte了,其中有6 byte浪費在填補空間上。相同的道理使得class A3 的大小是16 byte ,其中9 byte用於填補空間。
那麼也許你會問,既然「原樣性」會造成這樣多的空間浪費,那麼編譯器為什麼還要這樣做呢?其實這樣做是有它的必要的。我們考慮下面這種情況:
A1* pA1=new A1();
A1* pA2=new A2();
*pA1=*pA2;
我們定義了兩個A1型指針,一個指向A1對象,一個指向A2對象。現在我們執行一個默認的復制操作(復制一個個的成員變數),那麼這樣一個操作應該是把pA2所指的對象的A1那部分完全復制到pA1所指的對象里。假設編譯器不遵循「原樣性」,而是將派生類的成員和基類的成員捆綁在一起存放,去填補空間,那麼這樣的操作變會產生問題了。A1和A2都佔8 byte ,pA2會將其所指的8 byte空間里的內容全部復制給pA1所指的對象,那麼pA1所指的對象本來只有2個數據,3 byte的填補空間,而復制後卻變成了3個數據,2 byte的填補空間了,對於char c ,我們並不想把它復制過來的。這樣完全破壞了原語意,而這樣引起的bug幾乎是無法察覺的。
Ⅱ 計算機底層演算法是什麼
計算機最底層的原理是2進制的,只有1(通電)或0(斷電),計算機通過大量的與門、非門、或門、異或門、異非門來計算的。
Ⅲ 如何入手學習android 底層開發
android 底層開發學習:
一、基於Android的CPU+GPU的異構編程開發,目前主要有以下幾種平台:
1. OpenCL
在桌面系統和大規模並行計算領域被普遍使用的一種底層API。最近一段時間,主流的晶元廠商的旗艦或准旗艦晶元都開始支持OpenCL1.1或者1.2標准,包括高通,三星, 聯發科,Rockchip等廠商的晶元,都可以找到OpenCL的支持。
2. CUDA
目前只有NVIDIA自己出的基於Tegra K1晶元的設備(NVIDIA Shield)支持CUDA,所以支持的面比較窄。
3. RenderScript (RS)
Google力推的異構編程,宗旨是由平台幫你選擇運行的處理器,也就是說你是不知道你的程序跑在CPU還是GPU上的,這是由系統的驅動來決定的。想法是美好的,可現實是開發者並不買RS的帳, 大家覺得RS的性能不可控,靈活性太差,其文檔之缺乏也被人詬病;此外,晶元廠商對於RS的優化都還普遍處於比較低階的水平,這些都導致了RS在實際應用中很少被用到。
二、由於OpenCL的普及程度,以下部分只針對OpenCL展開。
目前支持OpenCL的設備和晶元
1. 支持OpenCL的GPU
高通幾乎全系的GPU, 包括但不限於以下GPU (Adreno 305, 320, 330, 405, 420, 430, 530 ...)
ARM Mali的6系和7系GPU, 比如T628, T760
2014年以來較新的Imagination PowerVR GPU,比如G6430
2. 支持OpenCL的晶元。以下是一個很粗略地列舉了主要的支持OpenCL的晶元。
高通8064, 8974(驍龍800,801), 8084(驍龍805), 8994(驍龍810)等
三星 Exynos獵戶座 5420, 5433 (內置ARM Mali GPU)
聯發科 MT6752 (內置ARM Mali T760 GPU)
瑞芯微 RK3288 (內置ARM Mali GPU)
3. 支持OpenCL的手機和平板。 這個就數不勝數了,下面只隨手給出幾個例子以供參考。(注意:Google Nexus系列的手機或平板,雖然硬體上支持OpenCL,但因為刪掉了OpenCL的驅動程序,所以基本都不支持OpenCL;值得注意的是,據國外blog上報道,可以將相應的OpenCL驅動推送回設備以重新開啟OpenCL的支持, 詳見maxlv.net 的頁面)
三星 Galaxy S4, S5, S6, Note 3, Note 4
LG G2, G3, G4
HTC One M7, M8, M9
小米使用高通晶元的手機和平板
魅族M1 Note
台積電P90HD
等等等等。。。。
如果不確定手頭的設備是否支持OpenCL, 可以使用OpenCL-Z Android進行檢測,這款軟體可以顯示詳細的OpenCL的設備信息,同時運行micro-benchmark檢測設備的計算能力。
三、需要掌握的知識:
1. 簡單的GPU基本知識
2. OpenCL並行程序設計
3. Android NDK知識
4. Android JNI介面的編寫
5. 簡單的Android程序開發知識
四、開發的步驟(這里只是步驟的精簡版本,只闡述操作,不進行解釋):
1. 編寫OpenCL的C/C++程序實現GPU的核心計算代碼
2. 用Android NDK編譯之前寫的C/C++代碼。這一階段可以在純C/C++環境下工作,可以編寫main函數測試實現的功能,用NDK將代碼編譯為可執行的代碼(BUILD_EXECUTABLE), 然後用ADB將可執行程序推送到設備上運行。運行可執行程序要求設備具有root許可權,如果沒有root許可權,可以通過Native Program Launcher (AndroidNativeLauncher · GitHub,可能需要翻牆) 這一工具在設備上執行二進制代碼。
3. 上一階段測試結束,功能基本正常。開始編寫JNI介面。
4. 開始編寫Android應用程序,使用JNI封裝native函數。編譯C/C++代碼成動態鏈接庫。
5. 在Android程序里,以靜態方式載入上一步編譯的動態鏈接庫。
6. 在需要的地方(比如點擊按鈕事件),調用相應的native函數,即可實現相應的功能。