Право на анализ escape / распределение стека с помощью Java 7
Я делаю несколько тестов с escape-анализом в Java 7, чтобы лучше понять, какие объекты имеют право на размещение в стеке.
Вот код, который я написал для тестирования размещения стека:
import java.util.ArrayList;
import java.util.Iterator;
public class EscapeAnalysis {
private static final long TIME_TO_TEST = 10L * 1000L; // 10s
static class Timestamp {
private long millis;
public Timestamp(long millis) {
this.millis = millis;
}
public long getTime() {
return millis;
}
public void setTime(long time) {
millis = time;
}
}
public static void main(String[] args) {
long r = 0;
System.out.println("test1");
r += test1();
System.out.println("test2");
r += test2();
System.out.println("test3");
r += test3();
System.out.println("test4");
r += test4();
System.out.println("test5");
r += test5();
System.out.println("test6");
r += test6();
System.out.println(r);
}
public static long test1() {
long r = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < TIME_TO_TEST) {
r += new Timestamp(System.currentTimeMillis()).getTime();
}
return r;
}
public static long test2() {
ArrayList<Integer> l = new ArrayList<Integer>(1000);
for (int i = 0; i < 1000; ++i) {
l.add(i);
}
long r = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < TIME_TO_TEST) {
for (Iterator<Integer> it = l.iterator(); it.hasNext(); ) {
r += it.next().longValue();
}
}
return r;
}
public static long test3() {
long r = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < TIME_TO_TEST) {
Timestamp ts = new Timestamp(System.currentTimeMillis());
ts.setTime(42);
r += ts.getTime();
}
return r;
}
public static long test4() {
ArrayList<Integer> l = new ArrayList<Integer>(1000);
for (int i = 0; i < 1000; ++i) {
l.add(i);
}
long r = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < TIME_TO_TEST) {
Iterator<Integer> it = l.iterator();
r += it.next().longValue();
r += it.next().longValue();
r += it.next().longValue();
r += it.next().longValue();
}
return r;
}
public static long test5() {
ArrayList<Integer> l = new ArrayList<Integer>(1000);
for (int i = 0; i < 1000; ++i) {
l.add(i);
}
long r = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < TIME_TO_TEST) {
Iterator<Integer> it = l.iterator();
for (int i = 0; i < l.size(); ++i) {
r += it.next().longValue();
}
}
return r;
}
public static long test6() {
long r = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < TIME_TO_TEST) {
for (Timestamp ts = new Timestamp(System.currentTimeMillis());
ts.getTime() > 0;
ts.setTime(ts.getTime() + System.currentTimeMillis())) {
r += ts.getTime();
}
}
return r;
}
}
И вот что он выводит с Java 7 на Linux
java -server -version
java version "1.7.0_02"
Java(TM) SE Runtime Environment (build 1.7.0_02-b13)
Java HotSpot(TM) 64-Bit Server VM (build 22.0-b10, mixed mode)
java -server -verbose:gc -XX:CompileThreshold=1 -cp bin EscapeAnalysis
test1
test2
[GC 15616K->352K(59776K), 0,0014270 secs]
[GC 15968K->288K(59776K), 0,0011790 secs]
[GC 15904K->288K(59776K), 0,0018170 secs]
[GC 15904K->288K(59776K), 0,0011100 secs]
[GC 15904K->288K(57152K), 0,0019790 secs]
[GC 15520K->320K(56896K), 0,0011670 secs]
[GC 15232K->284K(56256K), 0,0011440 secs]
test3
test4
test5
[GC 14876K->348K(55936K), 0,0005340 secs]
[GC 14620K->348K(56000K), 0,0004560 secs]
[GC 14300K->316K(55296K), 0,0004680 secs]
[GC 13948K->316K(55488K), 0,0003590 secs]
[GC 13692K->316K(54784K), 0,0004580 secs]
[GC 13436K->316K(54976K), 0,0005430 secs]
[GC 13180K->316K(54272K), 0,0004500 secs]
[GC 12924K->316K(54464K), 0,0005090 secs]
[GC 12668K->316K(53760K), 0,0004490 secs]
[GC 12412K->316K(53888K), 0,0004350 secs]
[GC 12156K->316K(53312K), 0,0005060 secs]
test6
6737499643744733086
Я использую журналы GC, чтобы узнать, были ли объекты размещены в стеке (идея Escape analysis в Java), которая может быть не на 100% надежной, но, кажется, дает хорошие подсказки.
По выводу, распределение стека работает для test1, test3, test4 и test6 и не работает для test2 и test5. Я не понимаю, почему это не работает с итератором в цикле for, хотя это работает
- с итератором вне цикла for (см. test4),
- с другим объектом внутри цикла for (см. test6).
Я прочитал код для итератора ArrayList, и я не понимаю, почему он не подходит для размещения в стеке в тестах 2 и 5, поскольку он не экранирует ни текущий метод, ни текущий поток.
Любая идея?
3 ответа
EA - это то, что анализирует компилятор C2, основываясь на IR, который он генерирует, поэтому вам нужно его скомпилировать, чтобы воспользоваться преимуществами. Каждый тест вызывается только один раз, поэтому у него нет шансов для его компиляции. Подробная информация о EA и C2 IR в вики внутренней сети горячей точки ( https://wikis.oracle.com/display/HotSpotInternals/Overview+of+Ideal,+C2, +C2 's + высокий уровень + промежуточное + представление и https://wikis.oracle.com/display/HotSpotInternals/EscapeAnalysis)
Вот версия, которая пытается показать влияние
import com.sun.management.ThreadMXBean;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Iterator;
public class EscapeAnalysisTest {
private static final long TIME_TO_TEST = 10L * 1000L; // 10s
static class Timestamp {
private long millis;
public Timestamp(long millis) {
this.millis = millis;
}
public long getTime() {
return millis;
}
public void setTime(long time) {
millis = time;
}
}
public static void main(String[] args) {
System.out.println("****");
doIt();
System.out.println("****");
doIt();
System.out.println("****");
doIt();
System.out.println("****");
doIt();
System.out.println("****");
}
private static void doIt() {
final ThreadMXBean mxbean = (ThreadMXBean) ManagementFactory.getThreadMXBean();
final long tid = Thread.currentThread().getId();
long r = 0;
final long allocPre = mxbean.getThreadAllocatedBytes(tid);
r += test1();
long alloc1 = mxbean.getThreadAllocatedBytes(tid);
System.out.println("test1 - " + (alloc1 - allocPre));
r += test2();
final long alloc2 = mxbean.getThreadAllocatedBytes(tid);
System.out.println("test2 - " + (alloc2 - alloc1));
r += test3();
final long alloc3 = mxbean.getThreadAllocatedBytes(tid);
System.out.println("test3 - " + (alloc3 - alloc2));
r += test4();
final long alloc4 = mxbean.getThreadAllocatedBytes(tid);
System.out.println("test4 - " + (alloc4 - alloc3));
r += test5();
final long alloc5 = mxbean.getThreadAllocatedBytes(tid);
System.out.println("test5 - " + (alloc5 - alloc4));
r += test6();
final long alloc6 = mxbean.getThreadAllocatedBytes(tid);
System.out.println("test6 - " + (alloc6 - alloc5));
System.out.println(r);
}
public static long test1() {
long r = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < TIME_TO_TEST) {
r += new Timestamp(System.currentTimeMillis()).getTime();
}
return r;
}
public static long test2() {
ArrayList<Integer> l = new ArrayList<Integer>(1000);
for (int i = 0; i < 1000; ++i) {
l.add(i);
}
long r = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < TIME_TO_TEST) {
for (Iterator<Integer> it = l.iterator(); it.hasNext(); ) {
r += it.next().longValue();
}
}
return r;
}
public static long test3() {
long r = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < TIME_TO_TEST) {
Timestamp ts = new Timestamp(System.currentTimeMillis());
ts.setTime(42);
r += ts.getTime();
}
return r;
}
public static long test4() {
ArrayList<Integer> l = new ArrayList<Integer>(1000);
for (int i = 0; i < 1000; ++i) {
l.add(i);
}
long r = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < TIME_TO_TEST) {
Iterator<Integer> it = l.iterator();
r += it.next().longValue();
r += it.next().longValue();
r += it.next().longValue();
r += it.next().longValue();
}
return r;
}
public static long test5() {
ArrayList<Integer> l = new ArrayList<Integer>(1000);
for (int i = 0; i < 1000; ++i) {
l.add(i);
}
long r = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < TIME_TO_TEST) {
Iterator<Integer> it = l.iterator();
for (int i = 0; i < l.size(); ++i) {
r += it.next().longValue();
}
}
return r;
}
public static long test6() {
long r = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < TIME_TO_TEST) {
for (Timestamp ts = new Timestamp(System.currentTi());
ts.getTime() > 0;
ts.setTime(ts.getTime() + System.currentTimeMillis())) {
r += ts.getTime();
}
}
return r;
}
}
который генерирует следующий вывод при запуске с -server -XX:CompileThreshold=1
****
test1 - 109048
test2 - 89243416
test3 - 16664
test4 - 42840
test5 - 71982168
test6 - 1400
-5351026995119026839
****
test1 - 16432
test2 - 85921464
test3 - 16664
test4 - 42840
test5 - 66777600
test6 - 1368
7844020592566674506
****
test1 - 48
test2 - 18256
test3 - 272
test4 - 18264
test5 - 18264
test6 - 272
-2137858376905291730
****
test1 - 48
test2 - 18256
test3 - 272
test4 - 18264
test5 - 18264
test6 - 272
3273987624143297143
****
одна опасность здесь в том, что компиляция этого метода изменила его более фундаментально, я не пытался защититься от этого, поэтому некоторое использование LogCompilation
или же PrintCompilation
может потребоваться проверить.
Я только что исследовал то же самое, но для Java 8. Я поместил свой ответ в дубликат вопроса, так как я не нашел его вовремя.
Резюме из полного ответа:
Прежде всего, это зависит от реализации. Этот ответ относится к OpenJDK 1.8 и, возможно, также к Oracle JVM 1.8.
Во-вторых, как уже говорили другие, распределение стека происходит только тогда, когда метод компилируется компилятором C2, что происходит только после того, как метод был вызван достаточное количество раз.
Если это так, объекты могут быть размещены в стеке, если
- все вызовы методов, которые его используют
- он никогда не присваивается никаким статическим или объектным полям, только локальным переменным (параметры для встроенных вызовов методов становятся локальными переменными)
- в каждой точке программы локальные переменные, содержащие ссылки на объект, должны определяться во времени JIT и не зависеть от непредсказуемого потока условного управления.
- Если объект является массивом, его размер должен быть константой времени JIT, а для индексации в нем должны использоваться константы времени JIT.
Встраивание особенно непредсказуемо, если вы не знаете некоторые специфические особенности Hotspot. Смотрите связанный ответ для некоторых деталей.
Редактировать: я попытался запустить ваш тест на Java 8 (OpenJDK), и все там встроено. Таким образом, существуют различия в распределении стека между Java 7 и 8.
Escape Analysis в значительной степени опирается на встраивание вызовов функций.
Как и с любым другим микробенчмарком - особенно на ВМ сервера - требуется прогрев. Если вы удалите -XX:CompileThreshold=1
и выполнив основной тест в цикле, вы заметите, что после 1-2 итераций он прекратит сбор мусора, потому что компилятор собрал достаточно информации для профилирования, чтобы встроить методы и затем выполнить анализ escape.