2014-06-19 18 views
6

Próbowałem zoptymalizować wykorzystanie pamięci RAM w grze na Androida, zmieniając prymitywy int na szorty. Zanim to zrobiłem interesowałem się wydajnością typów pierwotnych w Javie.Dlaczego krótki typ pierwotny jest znacznie wolniejszy od długiego lub int?

Stworzyłem ten mały testowy benchmark używając biblioteki zacisków.

public class BenchmarkTypes extends Benchmark { 

    @Param("10") private long testLong; 
    @Param("10") private int testInt; 
    @Param("10") private short testShort; 


    @Param("5000") private long resultLong = 5000; 
    @Param("5000") private int resultInt = 5000; 
    @Param("5000") private short resultShort = 5000; 

    @Override 
    protected void setUp() throws Exception { 
     Random rand = new Random(); 

     testShort = (short) rand.nextInt(1000); 
     testInt = (int) testShort; 
     testLong = (long) testShort; 
    } 

    public long timeLong(int reps){ 
     for(int i = 0; i < reps; i++){ 
      resultLong += testLong; 
      resultLong -= testLong;   
     } 
     return resultLong; 
    } 

    public int timeInt(int reps){ 
     for(int i = 0; i < reps; i++){ 
      resultInt += testInt; 
      resultInt -= testInt;   
     } 
     return resultInt; 
    } 

    public short timeShort(int reps){ 
     for(int i = 0; i < reps; i++){ 
      resultShort += testShort; 
      resultShort -= testShort; 
     } 
     return resultShort; 
    } 
} 

Wyniki testu zaskoczyły mnie.

okoliczności testowe

Benchmark uruchomić w bibliotece zacisku. Wynika

test

https://microbenchmarks.appspot.com/runs/0c9bd212-feeb-4f8f-896c-e027b85dfe3b

Int 2.365 ns

Długie 2.436 ns

Krótki 8.156 ns

test konkluzja?

Krótki typ pierwotny jest znacznie wolniejszy (3-4 ~ razy) niż typ pierwotny long i int pierwotny?

Pytanie

  1. Dlaczego jest krótki prymitywny znacznie wolniej niż int lub długo? Spodziewam się, że typ pierwotny int będzie najszybszy na 32-bitowej maszynie wirtualnej, a długi i krótki będzie równy w czasie lub krótki, aby być jeszcze szybszy.

  2. Czy dotyczy to również telefonów z systemem Android? Wiedząc, że telefony z systemem Android działają na ogół w środowisku 32-bitowym, a teraz w dzień coraz więcej telefonów zaczyna być dostarczanych z procesorami 64-bitowymi.

+1

Nie rozgrzałeś JIT. Nie wykonałeś wystarczającej liczby iteracji. To nie jest tak, jak mikrobisz Javę. –

+1

Jest (najprawdopodobniej) spowodowany przez konwersję Java krótki (za każdym razem) na int (lub długi) dla operacji arytmetycznych –

+0

@GermannArlington - Nie. Prawdziwym wyjaśnieniem różnicy czasów 1000x jest to, że benchmark jest niepoprawnie zapisany. Zobacz powiązane pytania i odpowiedzi. –

Odpowiedz

5

kodu bajtowego Javy nie wsparcia podstawowe operacje (+, -, *, /, >> >>>, < <,%) na prymitywnych typów mniejszych niż int. Po prostu nie ma kodów bajtowych przydzielonych do takich operacji w zestawie instrukcji. Zatem VM musi przekonwertować short (s) na int (s), wykonać operację, następnie obcina int back do short i przechowuje w wyniku.

Sprawdź wygenerowany kod bajtowy za pomocą javap, aby zobaczyć różnicę między testami krótkimi i wewnętrznymi.

Optymalizacje VM/JIT są wyraźnie silnie ukierunkowane na operacje int/long, co ma sens, ponieważ są najczęstsze.

Typy mniejsze niż int mają swoje zastosowania, ale przede wszystkim do zapisywania pamięci w tablicach. Nie są one tak dobrze dopasowane jak zwykli członkowie klasy (oczywiście nadal używasz ich, gdy są odpowiednim typem danych). Mniejsze elementy może nie zmniejszają nawet rozmiaru obiektów.Obecne maszyny wirtualne są (ponownie) dostosowane głównie do prędkości wykonania, więc maszyna wirtualna może nawet wyrównać pola do granic słowa maszynowego natywnego w celu zwiększenia wydajności dostępu kosztem wydatków na pamięć.

3

Jest to możliwe ze względu na sposób, w jaki Java/Android obsługuje arytmetyczne liczby całkowite w odniesieniu do prymitywów, które są mniejsze niż int.

Gdy dwa jajeczne są dodawane w java, które są typu danych, który jest mniejszy niż int, są one automatycznie promowane na liczbę całkowitą typu. Zazwyczaj wymagana jest obsada do przekształcenia wyniku z powrotem w wymagany typ danych.

Sztuką jest wyposażony skrót operacji jak +=, -= i tak dalej, gdzie obsada dzieje niejawnie takie, że ostateczny wynik operacji:

resultShort += testShort; 

rzeczywiście przypomina coś takiego:

resultShort = (short)((int) resultShort + (int) testShort); 

Jeśli spojrzymy na zdemontowany kod bajtowy metody:

public static int test(int a, int b){ 
    a += b; 
    return a; 
} 

widzimy:

public static int test(int, int); 
    Code: 
     0: iload_0  
     1: iload_1  
     2: iadd   
     3: istore_0  
     4: iload_0  
     5: ireturn 

porównując to do samej metody z typem danych zastąpiona przez krótki otrzymujemy:

public static short test(short, short); 
    Code: 
     0: iload_0  
     1: iload_1  
     2: iadd   
     3: i2s   
     4: istore_0  
     5: iload_0 
     6: ireturn 

Wskazówki dodatkowe wskazówki i2s (integer na krótko). Jest to prawdopodobny powód utraty wydajności. Inną rzeczą, którą można zauważyć, jest to, że wszystkie instrukcje są oparte na liczbach całkowitych, oznaczone przez prefiks i (np. iadd, co oznacza integer-add). Co oznacza, że ​​gdzieś podczas fazy iload szorty zostały awansowane na liczby całkowite, co prawdopodobnie spowoduje również pogorszenie wydajności.

Jeśli możesz wziąć moje słowo za to, kod bajtowy dla długich arytmetyki jest identyczny z liczbą całkowitą z wyjątkiem, że instrukcje są długie specyficzne (np. ladd zamiast iadd).

+1

Ta odpowiedź jest na dobrej drodze. Należy jednak pamiętać, że JVM nie wykonuje bezpośrednio bajtów (po skompilowaniu JIT). W związku z tym kody bajtowe nie wyjaśniają bezpośrednio różnicy. Na przykład, jeśli natywny zestaw instrukcji miał bezpośrednie wsparcie dla 16 arytmetycznych, a kompilator JIT był wystarczająco inteligentny, aby go użyć, to można by oczekiwać, że arytmetyka "krótka" będzie szybsza niż arytmetyczna "długa". –

+0

Ale rzeczywistość jest taka, że ​​zestawy instrukcji dla komputerów PC, serwerów, a nawet smartfonów są dostrajane do operacji 32-/64-bitowych, a nie 16-bitowych. W związku z tym wykonywanie 16-bitowej arytmetyki wymaga zwykle więcej * natywnych * instrukcji i więcej cykli zegara, co sprawia, że ​​jest wolniejszy niż 32 i 64-bitowy ... jak pokazuje benchmarking OP. Ale w dużej mierze zależy to od sprzętu platformy docelowej ... i potencjalnie od kompilatora JIT. –

Powiązane problemy