国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

關注性能:宏性能基準測試

2019-11-18 15:08:07
字體:
來源:轉載
供稿:網友

  字節碼提供了應用程序性能的線索
級別:中級

Jack Shirazi (jack@javaPerformanceTuning.com),董事,JavaPerformanceTuning.com
Kirk Pepperdine (kirk@JavaPerformanceTuning.com),首席技術官,JavaPerformanceTuning.com

熱衷于 Java 性能的 Jack Shirazi 和 Kirk Pepperdine ?? JavaPerformanceTuning.com 的董事和 CTO ?? 跟蹤遍布 Internet 上的性能討論,探究是什么在困擾著開發人員。在瀏覽 Usenet 新聞組 comp.lang.java 時,他們碰到了幾個有意思的底層性能調整問題。在 關注性能 的這篇文章中,他們對字節碼作了一些分析,檢驗并回答了其中的一些問題。
盡管沒有專門針對 Java 性能的 Usenet 討論組,但是有許多關于性能調整和優化的討論。這些討論中很大一部分基于從宏性能基準測試中得到的結果,所以在本月的專欄中我們也預備談論有關宏基準測試(microbenchmarking)的好處和不足之處。

前置還后置?
有一個問題非凡引起我們注重:哪一種運算更快:i++ 還是 ++i?在我們瀏覽過的幾乎每一個論壇中都可以看到以不同的形式提出的這個問題。雖然這個問題很簡單,但是看來沒有一個絕對的答案。

首先介紹一下它們的區別,++i 使用前置增量運算符,而 i++ 使用后置增量運算符。雖然它們都增加變量 i,但是前置增量運算符在增量運算之前返回 i 的值,而后置增量運算符在增量運算之后返回值 i。一個簡單的測試程序展示了這種區別:

public class Test {
public static void main(String[] args) {
int PRe = 1;
int post = 1;
System.out.println("++pre = " + (++pre));
System.out.println("post++ = " + (post++));
}
}

運行 Test 類生成以下的輸出:

++pre = 2
post++ = 1

宏基準測試
難道不能試著反復運行每次運算,并觀察哪一種運算有更快的運行時嗎?簡單的回答是能,但是危險在于宏基準測試并不總是測量您想要它們測量的內容。相當多的時候,即時(JIT)編譯器的優化和變化掩蓋了底層性能中所有可檢測的差異。例如,一個這種測試顯示第二個 i++ 運算比第一個 ++i 測試更快。但是改變測試順序顯示正好相反的結果!從這里我們只能得出測試方法有缺陷的結論。進一步的調查表明,這種令人困惑的結果來源于在第一次測試時發生的 HotSpot 優化。這些優化有雙重效果:使第一次運行增加了額外的開銷,并去掉了第二次運行時的解釋成本。

宏基準測試的其他變化,如在發生了 JIT 啟動成本后重復測試,在反復運行時只能給出不確定的結果。它可能告訴我們兩種運算符在速度上沒有區別,但是我們對此不能確定。

iinc 字節碼運算符
Heinz Kabutx 博士在其新聞信 The Java Specialists Newsletter 中,問他的讀者哪一個更快: i++、++i 還是 i+=1?在 Issue 64 中,他報告說有一位讀者用一種簡單的技術回答了他的問題:查看編譯的字節碼。事實上,他考察了四種增量語句:

++i;
i++;
i -= -1;
i += 1;

可以用 Java SDK 所帶的反匯編程序 javap 很輕易地分析編譯的字節碼。這四種增量語句的每一種得到的字節碼都是 iinc 1 1。

iinc 運算符有兩個參數。第一個參數指定變量在 JVM 的局部變量表中的索引,第二個參數指定變量的增量值。

這是不是給了我們一個明確的回答?無論如何,假如不同的源代碼編譯為同樣的字節碼,那么在速度上沒有區別,是不是?

運算符上下文
那么,假如代碼片斷都編譯為同樣的字節碼,使用不同的運算符的意義何在?好,讓我們回過頭看前置增量符和后置增量符。要害的一點是什么時候訪問變量。假如不訪問變量,那么這些運算符之間就沒有什么區別。語句 i++ 和 ++i 本身在功能上是一樣的。不過,語句 j=i++ 和 j=++i 在功能上是 不一樣的。我們需要分析在額外的賦值上下文中的字節碼。考慮這兩個類似的方法:

public static int preIncrement() {
int i = 0, j;
j = ++i;
return j;
}
public static int postIncrement() {
int i = 0, j;
j = i++;
return j;
}

反匯編 preIncrement() 和 postIncrement() 得到下面的字節碼:

Method int preIncrement()
0 iconst_0
1 istore_0
2 iinc 0 1
5 iload_0
6 istore_1
7 iload_1
8 ireturn

Method int postIncrement()
0 iconst_0
1 istore_0
2 iload_0
3 iinc 0 1
6 istore_1
7 iload_1
8 ireturn

現在我們 可以 看到這兩種方法間的區別:preIncrement() 返回 1,而 postIncrement() 返回 0。讓我們分析字節碼,更好地理解這種區別。首先,我們將解釋在反匯編的代碼中可以看到的不同字節碼運算符。

字節碼運算:i=0
iconst_0 運算符將整數 iconst_0 推到堆棧上。要完全理解這一點,請記住 JVM 模擬一個基于堆棧的 CPU(假如您以前沒接觸過堆棧,請參閱 java.util.Stack 類文檔)。JVM 在需要以后對某些東西進行操作時,先將它們推到堆棧中,在預備對它們進行操作時彈出它們。

在 Java 語言中有幾種不同的數據類型,對于不同的數據類型有不同的字節碼運算符。對于某些特定的優化,值 -1、0、1、2、3、4 和 5 都有專門的字節碼。假如我們不是處理這些值,那么編譯器會生成 bipush 字節碼運算,將一個特定的整數推到堆棧上(例如,假如方法的第一條語句是 int i = -2,那么第一個字節碼將會 bipush -2)。

下一條語句 istore_0 看上去可能像另一個處理整數 -1 到 5 的非凡字節碼,但是事實上,這次 _0 指向一個到局部變量表的索引。JVM 維護一個局部于方法的變量表,字節碼 istore 在堆棧的頂部彈出這個值,并將這個值儲存到局部變量表中。在這里我們用的是 istore_0,所以這個值儲存在表的索引 0 處。

所有這些解釋針對的是“i=0”的Java 字節碼,它被轉換為字節碼:

0 iconst_0
1 istore_0

更多的字節碼運算
現在我們知道了堆棧和局部變量表,我們可以更快地討論其他字節碼。正如我們前面說的,字節碼 iinc 0 1 在局部變量表索引 0 處增量值 1,iload_0 將局部變量表索引 0 處的值推到椎棧中,而 ireturn 從堆棧中彈出這個值,并將它推到調用方法的操作數堆棧上。下面的表 1 概括了字節碼。

表 1. 字節碼

字節碼 描述
iconst_0 將 0 推到堆棧中
iconst_1 將 1 推到堆棧中
istore_0 從堆棧中彈出這個值,并將它存儲到局部變量表的索引 0 處
istore_1 從堆棧中彈出這個值,并將它存儲到局部變量表的索引 1 處
iload_0 將局部變量表索引 0 處的值推到堆棧中
iload_1 將局部變量表索引 1 處的值推到堆棧中
iadd 從操作數堆棧中彈出兩個整數并讓它們相加。將得到的整數推回堆棧中
iinc 0 1 局部變量表索引 0 處的變量加 1
ireturn 從堆棧中彈出值并將它推到調用方法的操作數棧中。退出方法

比較方法
現在,讓我們再看一下這些反匯編的字節碼。我們將用 lvar 表示局部變量表,就像它是一個 Java 數組,并對字節碼加上注釋:

Method int preIncrement()
0 iconst_0 //push 0 onto the stack
1 istore_0 //pop 0 from the stack and store it at lvar[0], i.e. lvar[0]=0
2 iinc 0 1 //lvar[0] = lvar[0]+1 which means that now lvar[0]=1
5 iload_0 //push lvar[0] onto the stack, i.e. push 1
6 istore_1 //pop the stack (value at top is 1) and store at it lvar[1], i.e. lvar[1]=1
7 iload_1 //push lvar[1] onto the stack, i.e. push 1
8 ireturn //pop the stack (value at top is 1) to the invoking method i.e. return 1

Method int postIncrement()
0 iconst_0 //push 0 onto the stack
1 istore_0 //pop 0 from the stack and store it at lvar[0], i.e. lvar[0]=0
2 iload_0 //push lvar[0] onto the stack, i.e. push 0
3 iinc 0 1 //lvar[0] = lvar[0]+1 which means that now lvar[0]=1
6 istore_1 //pop the stack (value at top is 0) and store at it lvar[1], i.e. lvar[1]=0
7 iload_1 //push lvar[1] onto the stack, i.e. push 0
8 ireturn //pop the stack (value at top is 0) to the invoking method i.e. return 0

現在,希望您能更清楚地了解所發生的事情,以及方法之間的一些功能差別。惟一的差別是兩個方法的第三個和第四個字節碼交換了。注釋的字節碼清楚表明,在 postIncrement() 方法中,iinc 運算完全是多余的,因為從這一點起,不再使用被更新的局部變量元素 lvar[0]。對于這個特定的方法,一個優化 JIT 編譯程序可以完全去掉這種字節碼運算。所以在這種特定情形中,postIncrement() 方法可能有比 preIncrement() 操作更少的字節碼運算,從而使它更加高效。但是在大多數使用后置增量運算符的情況下,增量運算是不能優化的。

那么誰更快呢?
我們學到了什么?是的,假如語句只有 ++i 和 i++ ,那么它們之間沒有區別。只有在存在額外的賦值時,編譯的字節碼才會有區別。

在賦值的上下文中,比較前置增量運算符或者后置增量運算符的使用有可能得到不同的運行時。但是使用哪種運算的功能結果都不太可能是一樣的。記住,在我們這里的例子里,方法實際上返回不同的值,它取決于我們是使用前置增量運算符還是后置增量運算符。在一個普通程序中,其中一種變化可能會成為一個缺陷。

結束語
在過去,我們可以根據一組運算的語言表達對它們的成本進行測量。這是因為這些運算到底層運行時環境的轉換總是靜態的,這在 Java 運行時中是不成立的。Java 運行時可以動態優化運行的代碼,這是一種非凡強大的功能。盡管這種功能還沒有使我們完全不能進行宏性能基準測試,但是它導致我們在使用這種技術時需要更加當心。

參考資料

閱讀 Jack Shirazi 和 Kirk Pepperdine 的全部 關注性能 系列。

Greg Travis 的“如何封鎖您的(或打開別人的) Java 代碼”(developerWorks,2001 年 5 月)提供了有關反編譯一個 Java 類文件的信息。

利用 The Jikes Research Virtual Machine (developerWorks,2000 年 2 月)了解更多有關 IBM 對高性能 JVM 的研究。

Click 博士在 JavaOne 2003 上展示了 High Performance Computing with HotSpot Server Compiler (PDF)。

The Java HotSpot Virtual Machine, v1.4.1 (java.sun.com,2002 年 9 月)是有關 JVM HotSpot 技術的官方白皮書。

Jack Shirazi 的“Micro-Tuning Step-by-Step”(ONJava,2002 年 3 月)提供了對宏性能基準測試的實用建議。

在 developerWorks Java 技術專區 可以找到數百篇有關 Java 編程各個方面的文章。

關于作者
Jack Shirazi 是 JavaPerformanceTuning.com 的董事,也是 Java Performance Tuning (O′Reilly)一書的作者。

Kirk Pepperdine 是 Java Performance Tuning.com 的首席技術官,并且在過去 15 年一直關注對象技術和性能調優。Kirk 是 ANT Developer′s Handbook 一書的合著者。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 鹤峰县| 大洼县| 克东县| 宾阳县| 礼泉县| 庆云县| 买车| 青浦区| 兴山县| 临颍县| 郧西县| 济阳县| 临武县| 石家庄市| 忻州市| 建阳市| 灌南县| 祁东县| 嘉义县| 临高县| 田林县| 静乐县| 松原市| 江安县| 高尔夫| 承德市| 荣成市| 怀安县| 金门县| 南平市| 额济纳旗| 乡宁县| 阜城县| 华蓥市| 屯留县| 永川市| 特克斯县| 如皋市| 新昌县| 锡林浩特市| 武乡县|