java面試題之用最有效率的方法算出2乘以8等於幾
這是網上流傳的"變態級JAVA程式設計師面試32問"的其中一題(二十八題),然後下面給出來的答案是
第二十八,程式設計題: 用最有效率的方法算出2乘以8等於幾?
有C背景的程式設計師特別喜歡問這種問題。
2 << 3
粗看似乎很在理,大致想來2<<3會是移位操作,在Java的位元組碼中移位指令是ishl(右移),而在CPU上的硬體指令可能就會是shl(算術右移指令)。其實不然,如果熟悉組合語言,還考慮過編譯優化,2<<3根本不會使用移位操作,而是在編譯時就優化計算出16來了。
但如果是寫成這樣的方式(int i = 2; int j = i<<2;),又是不一樣的,大家可以從程式碼不同寫法生成的Java操作指令或彙編指令看出個端倪。
Java程式碼 | 對應的位元組碼指令 | 說明 |
int i = 2 <<3; | 0: bipush 16 | 編譯時就把2左移3位的置算出來了,可以說降低了 編譯時效率 棧中彈出int型值,存到位置為1的區域性變數中 |
int i = 2 * 8; | 與上同 | |
int i = 2; int j = i * 8; |
0: iconst_2 1: istore_1 2: iload_1 3: iconst_3 4: ishl 6: istore_2 |
常量2壓棧 棧中彈出int型值,存到位置為1的區域性變數中 位置為1的int型區域性變數壓棧 常量3壓棧 左移位操作,結果值壓棧 棧中彈出int型值,存到位置為2的區域性變數中 |
int i = 2; int j = i * 8; |
0: iconst_2 1: istore_1 2: iload_1 3: bipush 8 5: imul 6: istore_2 |
javac 生成的位元組碼操作指令是 imul,javac未作優化 |
所以把程式碼寫成 int i = 2 << 3,和寫成 int i = 16; 是一樣的,前者還有降低了編譯時效率
下面再舉例說明一下,VC++編譯器,其實也會作這樣的優化
C++ 程式碼 | 對應的彙編指令 | 說明 |
int i = 2 << 3; | mov dword ptr [ebp-4],10h | 也是把2<<3算出來了,為16(10h),放入記憶體 |
int i = 2 * 8; | 與上同 | |
int i = 2; int j = i << 3; |
mov dword ptr [ebp-4],2 mov eax,dword ptr [ebp-4] shl eax,3 mov dword ptr [ebp-8],eax |
把2放入記憶體中 SS:[ebp-4] 把 SS:[ebp-4] 整數傳送到eax暫存器中 對eax中的資料左移3位 再把移位後的結果存到記憶體 SS:[ebp-8]中 |
int i = 2; int j = i * 8 |
與上同 | VC++自動優化成移位操作,因為8為2的整數次冪 |
int i = 2; int j= i * 9; |
mov dword ptr [ebp-4],2 mov eax,dword ptr [ebp-4] imul eax,eax,9 mov dword ptr [ebp-8],eax |
生成的彙編指令為 imul ,因為9不是2的整數次冪 |
所以從回答說是 int i = 2 << 3; 並沒改變一點執行上的效率,因為它 int i = 16; 是完全一樣的。但如果寫成
int i = 2;
int j = i << 3;
比寫成
int i = 2;
int j = i * 8:
確實會提高一定的執行效率, 因為 i * 8,要用Java操作指令 imul 進行運算,這也只是說JDK的編譯器javac是這麼處理的。JDK編譯時並沒有 i * 8 優化成 i << 3 。
如果把程式碼換成是C++程式碼,也是寫成
int i = 2;
int j = i * 8:
由VC++編譯出來的彙編指令程式碼也還是用 shl eax, 3,它和寫成 int j = i << 3 全一致的。VC++從效率出發已經給你作了這樣的代優化,再回味一下這個問題的答案中的一句話"有C背景的程式設計師特別喜歡問這種問題。",可以獲知那個有C背景的程式設計師在這裡耍了一個"小聰明",卻又沒有佔到一點小便宜,只因為他低估了編譯器的優化功能。
之所以問這樣問題的人,他們只是基於這樣一個事實,整數乘法或整數除法所需要的時鐘週期遠遠大於移位操作所需的時鐘週期,下面列出這個指令的基本執行時間:
移位指令 暫存器移 1 位 時鐘週期數為 2
整數乘法 IMUL 16 位暫存器乘 時鐘週期數為 128 ~ 154
整數除法 IDIV 16 位暫存器 時鐘週期數為 165 ~ 184
而即使 Java編譯器在編譯 int j = i * 8; 時用的是 imul ,但真正執行這這段程式碼,由虛擬機器JVM轉換成原生代碼是時候會不會進一步優化成用移位操作的彙編指也未得而知,必要時當然可追蹤一下java.exe的執行過程,即使執行時會作此優化,在java中把 int j = i * 8 寫成 int j = i <<3 ,可獲取一點點的效率,微不足道。
任外,在Visual C++ .net 2003,還增加了對Intel Pentium 4 和 AMD Athlon的程式碼優化,當用 /G7 編譯時,可以產生了更快(但更長)的指令序列,避免了使用 imul 指令,該指令在 Intel Pentium 4 上具有 14 個週期的滯後時間。
如 i * 9,可能被編譯成如下程式碼
mov ecx, dword ptr _i$[esp-4]
mov eax, ecx
shl eax, 3
add eax, ecx
本來應該是 imul 乘法指令,用/G7編譯選項巧妙的生成了先左移3位,再加上原來的值。網上介紹的是這麼說的,可以我在Visual C++ .net 2003,用/G7選項編譯時卻沒有生成與上類似的彙編程式碼,仍然是生成的 imul 指令。
最後再從業務出發,比如說這樣一個簡單需求,A的工資是B的工資的8倍(B可是個低階奴隸了),這種情況下要你寫程式碼是寫成
salaryA = salaryB * 8;
還是寫成
salaryA = salaryB << 3;
這種問題不用回答了吧。
如果說用 int j = i <<3 的形式的情況有沒有呢,也有,一般是在處理位元組的時候,比如所有位整體左移3位,低3位再用位元組填充,但也比較少牽涉到 8 倍這個概念。
我昨日就到一個公司面試,看了一些面試題(包括系統分析題),真不太想做,我那時候就跟他們說過:我一直以來就不太喜歡做題,做題的時候可能常常不是關注問題本身,還得留個意去揣摩出題人的用意。
象中國人的英語考試中的選擇題那樣,如果四個答案是分別是
A: aab B: aac C: aad D: 456
一般老師會告訴你用排除法排除答案D,因為D太不相似了。