举例的时候要小心

下午翻了一下老资料,又读了一遍JVM Language Summit 2008上John Pampuch做的一个演示稿: VM Optimizations for Language Designers。读到第18页的时候都还没觉得有什么问题,但第20-21页演示的常量折叠在JDK6的HotSpot里明明没有实现……数组的创建也在sum()方法里的时候,开上EA,才能看到演示稿中所描述的效果;private static final int[]的话HotSpot只是展开了循环却没有对数组内容做常量折叠:因为光看sum()方法无法知道数组元素有没有变过。所以说读这些演示稿的时候要很小心,把里面提到的内容都自己验证一次。

把虚方法内联、循环展开、逃逸分析+标量替换、常量折叠等优化的演示放在一起做个例子。上述演示稿中的例子大致跟下面的FooA版bar()一样。

public class TestC2ConstantFolding {
    private static void driver() {
        IFoo[] array = new IFoo[] {
            new FooA(), new FooB(), new FooC(), new FooD()
        };
        for (int i = 0; i < 100000; i++) {
            array[i % array.length].bar(); // megamorphic callsite to prevent inlining
        }
    }
    
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 1000; i++) {
            driver();
        }
        System.in.read();
    }
}

interface IFoo {
    int bar();
}

class FooA implements IFoo {
    private static final int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    public int number(int i) {
        return numbers[i];
    }
    
    public int bar() {
        int sum = 0;
        for (int i = 0; i < numbers.length; i++) {
            sum += number(i);
        }
        return sum;
    }
}

class FooB implements IFoo {
    private static final int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    public int number(int i) {
        return numbers[i];
    }
    
    public int bar() {
        int sum = 0;
        int i = 0;
        
        sum += number(i++);
        sum += number(i++);
        sum += number(i++);
        sum += number(i++);
        sum += number(i++);
        sum += number(i++);
        sum += number(i++);
        sum += number(i++);
        sum += number(i++);
        sum += number(i++);
        
        return sum;
    }
}

class FooC implements IFoo {    
    public int bar() {
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        int sum = 0;
        for (int i = 0; i < numbers.length; i++) {
            sum += numbers[i];
        }
        return sum;
    }
}

class FooD implements IFoo {
    public int bar() {
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        int sum = 0;
        int i = 0;
        
        sum += numbers[i++];
        sum += numbers[i++];
        sum += numbers[i++];
        sum += numbers[i++];
        sum += numbers[i++];
        sum += numbers[i++];
        sum += numbers[i++];
        sum += numbers[i++];
        sum += numbers[i++];
        sum += numbers[i++];
        
        return sum;
    }
}


在Sun的JDK 6u17和6u21的server模式上跑,只有FooD版的bar()方法最终生成的代码与开头说的演示稿的示例一样,是:
public class FooD implements IFoo {
    public int bar() {
        // safepoint inserted here
        
        return 55; // constant-folded
    }
}


FooC的版本虽然有常量折叠,但数组的创建却没削除掉,会试图在thread-local storage上为这个数组分配空间:
public class FooC implements IFoo {
    public int bar() {
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        
        // safepoint inserted here
        
        return 55; // constant-folded
    }
}

这里到底跟FooD版差在什么地方我还没弄清楚……得用IGV仔细看看才行了。

FooA与FooB版本的基本上一样,都如预期般把number()方法内联了,并且做了循环展开+冗余数组边界检查削除,但没有对数组内容求和这个操作进行常量折叠——因为numbers是静态变量,虽然声明为final使得该变量只可能指向一个固定的数组,但数组的内容仍然是可变的,至少要在类一级做分析才可以确定没有别的方法修改过该数组的元素,而HotSpot的代码分析都是在方法一级做的,就无能为力了。生成的代码基本上等价于:
class FooA implements IFoo {
    private static final int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    public int bar() {
        int sum = 0;
        
        sum += numbers[0];
        sum += numbers[1];
        sum += numbers[2];
        sum += numbers[3];
        sum += numbers[4];
        sum += numbers[5];
        sum += numbers[6];
        sum += numbers[7];
        sum += numbers[8];
        sum += numbers[9];
        
        
        // safepoint inserted here
        
        return sum;
    }
}

// FooB is the same

注意在实际生成的代码里数组越界检查的开销已经完全削除了,这里用Java代码表现不出来。

于是那个演示稿不只是“over simplification”,而是演示了一个尚未实现的优化(虽说那优化不是不可能做到)。

======================================================

出于好奇心,顺带拿JRockit R28来对比测试了一下。确认了上面的代码通过在一个callsite调用同一接口方法的多个实现能有效阻止JRockit把我要测的方法内联到“热身”代码中。
以前一直没拿JRockit来玩过,只是读过些资料。实际一玩觉得在JIT上与预期的表现有落差。上面4个版本的代码在JRockit的全优化模式下编译出来形如:
class FooA implements IFoo {
    private static final int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    public int bar() {
        // safepoint inserted here
        
        int sum = 0;
        for (int i = 0; i < 10; i++) { // numbers.length is constant-propagated
            // safepoint inserted here
            
            sum += numbers[i]; // array bounds check not eliminated
        }
        return sum;
    }
}

class FooB implements IFoo {
    private static final int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    public int bar() {
        // safepoint inserted here
        
        int sum = 0;
        
        sum += numbers[0]; // array bounds check not eliminated
        sum += numbers[1]; // array bounds check not eliminated
        sum += numbers[2]; // array bounds check not eliminated
        sum += numbers[3]; // array bounds check not eliminated
        sum += numbers[4]; // array bounds check not eliminated
        sum += numbers[5]; // array bounds check not eliminated
        sum += numbers[6]; // array bounds check not eliminated
        sum += numbers[7]; // array bounds check not eliminated
        sum += numbers[8]; // array bounds check not eliminated
        sum += numbers[9]; // array bounds check not eliminated
        
        return sum;
    }
}

class FooC implements IFoo {    
    public int bar() {
        // safepoint inserted here
        
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        int sum = 0;
        for (int i = 0; i < 10; i++) { // numbers.length is constant-propagated
            // safepoint inserted here
            
            sum += numbers[i]; // array bounds check not eliminated
        }
        return sum;
    }
}

class FooD implements IFoo {
    public int bar() {
        // safepoint inserted here
        
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        return 55; // constant-folded
    }
}

虚方法内联是做了,常量传播也有做,但循环展开、冗余数组越界检查的削除却不尽人意。JRockit R28为这4个版本的bar()方法生成的代码质量都比HotSpot生成的差一些。或许是这里的预热方式还不够适合JRockit,但我试过强制指定优化模式还是得到了一样的结果。要摸清这位大小姐的脾气看来还得花不少时间……

==============================================================

P.S. Sovereign和J9里有field privatization,不知道能不能实现这例子里的优化呢。

你可能感兴趣的:(jvm,jdk,thread,Blog,sun)