Zaujalo mě tohle:
[code]
0x009bac9a: cmp $0x7ffffffe,%ecx
0x009baca0: jg 0x009bad15 ;*iload_1
[/code]
To se snaží JVM detekovat nekonečný cyklus? Co znamená pak ten call na OopMap? Zkoušel jsem Oracle JVM a nic zvláštního neprovede, podle očekávání se zacyklí. OpenJDK tu bohužel nemám.
Jinak opět to, co jsem psal myslím v předminulém díle - je pěkné, že si s unrolling dává tolik práce, ale potom stejně nafláká 3x závislé imul za sebou. Ten kód vypadá celkem jednoduše, bez nějakého overhead JVM, takže by asi bylo zajímavé jej porovnat třeba s gcc.
$ cat f.c ff.c
extern int f(int);
int main(void)
{
for (int i = 0; i < 1000000000; i++) {
f(15);
}
return 0;
}
int f(int n)
{
__asm__("" : : : "memory");
int r = 1;
if (n <= 1)
return 1;
for (int i = 1; i <= n; i++) {
r *= i;
}
return r;
}
$ cat A.java
public class A
{
public static void main(String[] args)
{
for (int i = 0; i < 1000000000; i++) {
fac(15);
}
}
public static int fac(int n)
{
int r = 1;
if (n <= 1)
return 1;
for (int i = 1; i <= n; i++) {
r *= i;
}
return r;
}
}
Tak na 1000000000x faktorial(15) jsou výsledky následující:
Oracle JVM 1.7.0_03-b05: 0m15.172s
cygwin gcc 4.5.3 -O1: 0m15.703s
cygwin gcc 4.5.3 -O[2-6]: 0m15.266s
cygwin gcc 4.5.3 -O[2-6] -funroll-loops: 0m11.875s
cygwin gcc 4.5.3 -O[2-6] -funroll-loops (pouze faktorial): 0m13.078s
Ten poslední jsem upravil, aby gcc nezoptimalizovalo i samotný cyklus kolem volání.
Jo, včetně startu JVM. Ale ten bere do setiny vteřiny, takže celkem zanedbatelné.
Já jsem spíš čekal, že ty výsledky budou srovnatelné, je to přecejen dost triviální algoritmus, kde není moc co zlepšit. Takže ani nečekám, že ICC by se lišilo (na i386, na SSE by možná dokázal paralelizovat líp, i když tam je zase problém s přetečením). Hlavní rozdíly, co dělá GCC při unroll jsou následující:
- lepší střídání instrukcí (předpočet operandu je vždy tři instrukce před násobením, zatímco JVM z článku předpočítá vše předem)
- lepší využití registrů (v podstatě všechny pomocné operandy střídá ve dvou registrech, JVM z článku použije jeden registr na každý unroll)
- unroll na osm cyklů (JVM z článku čtyři cykly, při tom využití registrů by byl větši unroll ostatně spíš kontraproduktivní)
Ještě doma vyzkouším Linux OpenJDK na 64bit, docela by mě zajímalo srovnání, co leze z Oracle hotspot.
Pěkné, globální optimalizace jsou dobrá věc. Škoda, že hlavně v takových triviálních případech :-)
Ostatně, gcc se nenechá příliš zahanbit, když jsem mu nechal funkci v hlavním souboru, tak si jednoduše projel cyklus a vracel konstantu. Pamatuju si, že Borland C++ měl pro inline podmínku, aby funkce neobsahovala cyklus. Holt doba trochu pokročila :-)
Ještě jsem zkoušel další nastavení GCC a platí, že méně je někdy více (všechno na základě posledního příkladu, ten -funroll-loops pouze na faktorial):
--param max-unroll-times=8 (default): 0m13.063s
--param max-unroll-times=4 (default): 0m10.750s
--param max-unroll-times=2 (default): 0m8.953s
Problém je zjevně v overhead na začátku, kdy se rozhoduje, kde začít. Tím, že je "n" poměrně nízké, je to defakto zdvojnásobení práce. Možná zvládají modernější kompilátory líp (binární vyhledávání, relativní vypočítaný skok, tabulka, ...).