Czy w javie bardziej wydajne jest używanie bajtu lub short zamiast int i float zamiast double?

91

Zauważyłem, że zawsze używałem int i double, bez względu na to, jak mała lub duża musi być liczba. Czy w Javie bardziej wydajne jest używanie, byteczy shortzamiast inti floatzamiast double?

Więc załóżmy, że mam program z dużą ilością int i double. Czy warto byłoby przejść i zmienić moje ints na bajty lub krótkie, gdybym wiedział, że liczba będzie pasować?

Wiem, że java nie ma typów bez znaku, ale czy jest coś więcej, co mógłbym zrobić, gdybym wiedział, że liczba będzie tylko dodatnia?

Przez wydajne rozumiem głównie przetwarzanie. Zakładam, że odśmiecacz byłby dużo szybszy, gdyby wszystkie zmienne miały połowę rozmiaru, a obliczenia prawdopodobnie też byłyby nieco szybsze. (Myślę, że ponieważ pracuję na Androidzie, muszę też trochę martwić się o pamięć RAM)

(Zakładam, że garbage collector zajmuje się tylko obiektami, a nie prymitywami, ale nadal usuwa wszystkie prymitywy z opuszczonych obiektów, prawda?)

Wypróbowałem to z małą aplikacją na Androida, ale tak naprawdę nie zauważyłem żadnej różnicy. (Chociaż niczego nie zmierzyłem „naukowo”).

Czy mylę się, zakładając, że powinno być szybsze i bardziej wydajne? Nie chciałbym przechodzić i zmieniać wszystkiego w ogromnym programie, aby dowiedzieć się, że zmarnowałem swój czas.

Czy warto by było zacząć od początku, kiedy zaczynam nowy projekt? (Chodzi mi o to, że myślę, że wszystko by pomogło, ale jeśli tak, to dlaczego nie wygląda na to, żeby ktokolwiek to robił.)

DisibioAaron
źródło

Odpowiedzi:

108

Czy mylę się, zakładając, że powinno być szybsze i bardziej wydajne? Nie chciałbym przechodzić i zmieniać wszystkiego w ogromnym programie, aby dowiedzieć się, że zmarnowałem swój czas.

Krótka odpowiedź

Tak, mylisz się. W większości przypadków nie ma to większego znaczenia pod względem wykorzystywanej przestrzeni.

Nie warto próbować tego optymalizować ... chyba że masz wyraźne dowody na to, że optymalizacja jest potrzebna. A jeśli musisz zoptymalizować użycie pamięci w szczególności przez pola obiektów, prawdopodobnie będziesz musiał podjąć inne (bardziej efektywne) środki.

Dłuższa odpowiedź

Modele maszyny wirtualnej języka Java układają stosy i pola obiektów przy użyciu przesunięć, które są (w efekcie) wielokrotnościami rozmiaru komórki pierwotnej 32-bitowej. Więc kiedy zadeklarujesz lokalną zmienną lub pole obiektu jako (powiedzmy) a byte, zmienna / pole będzie przechowywane w komórce 32-bitowej, podobnie jak int.

Istnieją dwa wyjątki od tej reguły:

  • longa doublewartości wymagają 2 prymitywnych komórek 32-bitowych
  • tablice typów pierwotnych są reprezentowane w postaci spakowanej, tak że (na przykład) tablica bajtów zawiera 4 bajty na słowo 32-bitowe.

Więc to może być warte korzystanie optymalizacja longi double... i duże tablice prymitywów. Ale generalnie nie.

Teoretycznie JIT może to zoptymalizować, ale w praktyce nigdy nie słyszałem o JIT, który to robi. Jedną z przeszkód jest to, że JIT zwykle nie może działać, dopóki nie zostaną utworzone instancje kompilowanej klasy. Gdyby JIT zoptymalizował układ pamięci, mógłbyś mieć dwa (lub więcej) „smaków” obiektów tej samej klasy… a to nastręczałoby ogromne trudności.


Revisitation

Spojrzenie na benchmark skutkuje odpowiedzią @ meriton, wydaje się, że użycie shorti bytezamiast intpociąga za sobą karę wydajności za mnożenie. Rzeczywiście, jeśli rozważasz operacje w oderwaniu, kara jest znacząca. (Nie powinieneś rozważać ich w oderwaniu ... ale to inny temat.)

Myślę, że wyjaśnienie jest takie, że JIT prawdopodobnie wykonuje mnożenie przy użyciu 32-bitowych instrukcji mnożenia w każdym przypadku. Ale w bytei shortprzypadku, wykonuje dodatkowe instrukcje do konwersji pośredniej wartości 32 bitowej na bytelub shortw każdej iteracji pętli. (Teoretycznie taka konwersja mogłaby zostać wykonana raz na końcu pętli ... ale wątpię, czy optymalizator byłby w stanie to rozgryźć).

W każdym razie, ten jest skierowany do innego problemu z przestawieniem się shorti bytejako optymalizacji. Może to pogorszyć wydajność ... w algorytmie, który jest intensywny arytmetycznie i obliczeniowo.

Stephen C.
źródło
30
+1 nie optymalizuje, chyba że masz wyraźne dowody na problem z wydajnością
Bohemian
Erm, dlaczego JVM musi czekać na kompilację JIT, aby spakować układ pamięci klasy? Ponieważ typy pól są zapisywane w pliku klasy, czy maszyna JVM nie mogłaby wybrać układu pamięci w czasie ładowania klasy, a następnie rozstrzygnąć nazwy pól jako przesunięcia bajtów zamiast słów?
meriton
@meriton - Jestem prawie pewien, że układy obiektów określane w czasie ładowania klasy i nie zmieniają się później. Zobacz część mojej odpowiedzi „drobnym drukiem”. Gdyby rzeczywisty układ pamięci zmienił się, gdy kod został poddany JIT, byłoby to naprawdę trudne dla maszyny JVM. (Kiedy powiedziałem, że JIT może zoptymalizować układ, jest to hipotetyczne i niepraktyczne ... co może wyjaśniać, dlaczego nigdy nie słyszałem, aby JIT faktycznie to robił.)
Stephen C
Wiem. Chciałem tylko wskazać, że chociaż układy pamięci są trudne do zmiany po utworzeniu obiektów, JVM może nadal optymalizować układ pamięci przedtem, tj. W czasie ładowania klasy. Innymi słowy, to, że specyfikacja JVM opisuje zachowanie maszyny JVM z przesunięciami słów, niekoniecznie oznacza, że ​​JVM musi być zaimplementowany w ten sposób - chociaż najprawdopodobniej tak jest.
meriton
@meriton - specyfikacja JVM mówi o „przesunięciu słów maszyny wirtualnej” w ramach lokalnych ramek / obiektów. Nie określono, w jaki sposób są one odwzorowywane na przesunięcia maszyn fizycznych. Rzeczywiście, nie może tego określić ... ponieważ mogą istnieć specyficzne dla sprzętu wymagania dotyczące wyrównania pól.
Stephen C,
29

Zależy to od implementacji maszyny JVM, a także sprzętu, na którym się ona znajduje. Większość współczesnego sprzętu nie pobiera pojedynczych bajtów z pamięci (lub nawet z pamięci podręcznej pierwszego poziomu), tj. Używanie mniejszych typów pierwotnych generalnie nie zmniejsza zużycia przepustowości pamięci. Podobnie, nowoczesny procesor ma rozmiar słowa wynoszący 64 bity. Mogą wykonywać operacje na mniejszej liczbie bitów, ale działa to poprzez odrzucanie dodatkowych bitów, co również nie jest szybsze.

Jedyną korzyścią jest to, że mniejsze typy pierwotne mogą skutkować bardziej zwartym układem pamięci, szczególnie w przypadku używania tablic. Oszczędza to pamięć, co może poprawić lokalność odniesienia (zmniejszając w ten sposób liczbę chybień w pamięci podręcznej) i zmniejszyć obciążenie związane z wyrzucaniem elementów bezużytecznych.

Ogólnie rzecz biorąc, używanie mniejszych typów pierwotnych nie jest jednak szybsze.

Aby to wykazać, spójrz na następujący wzorzec:

package tools.bench;

import java.math.BigDecimal;

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1; 
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }   

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        Benchmark[] benchmarks = {
            new Benchmark("int multiplication") {
                @Override int run(int iterations) throws Throwable {
                    int x = 1;
                    for (int i = 0; i < iterations; i++) {
                        x *= 3;
                    }
                    return x;
                }
            },
            new Benchmark("short multiplication") {                   
                @Override int run(int iterations) throws Throwable {
                    short x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x *= 3;
                    }
                    return x;
                }
            },
            new Benchmark("byte multiplication") {                   
                @Override int run(int iterations) throws Throwable {
                    byte x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x *= 3;
                    }
                    return x;
                }
            },
            new Benchmark("int[] traversal") {                   
                @Override int run(int iterations) throws Throwable {
                    int[] x = new int[iterations];
                    for (int i = 0; i < iterations; i++) {
                        x[i] = i;
                    }
                    return x[x[0]];
                }
            },
            new Benchmark("short[] traversal") {                   
                @Override int run(int iterations) throws Throwable {
                    short[] x = new short[iterations];
                    for (int i = 0; i < iterations; i++) {
                        x[i] = (short) i;
                    }
                    return x[x[0]];
                }
            },
            new Benchmark("byte[] traversal") {                   
                @Override int run(int iterations) throws Throwable {
                    byte[] x = new byte[iterations];
                    for (int i = 0; i < iterations; i++) {
                        x[i] = (byte) i;
                    }
                    return x[x[0]];
                }
            },
        };
        for (Benchmark bm : benchmarks) {
            System.out.println(bm);
        }
    }
}

który drukuje na moim nieco starym notatniku (dodawanie spacji w celu dostosowania kolumn):

int       multiplication    1.530 ns
short     multiplication    2.105 ns
byte      multiplication    2.483 ns
int[]     traversal         5.347 ns
short[]   traversal         4.760 ns
byte[]    traversal         2.064 ns

Jak widać, różnice w wydajności są niewielkie. Optymalizacja algorytmów jest znacznie ważniejsza niż wybór typu pierwotnego.

meriton
źródło
3
Zamiast mówić „przede wszystkim przy używaniu tablic”, myślę, że łatwiej byłoby to powiedzieć shorti bytesą bardziej wydajne, gdy są przechowywane w tablicach, które są wystarczająco duże, aby mieć znaczenie (im większa tablica, tym większa różnica w wydajności; a byte[2]może być więcej lub mniej wydajne niż an int[2], ale nie na tyle, aby mieć znaczenie w obu przypadkach), ale te indywidualne wartości są wydajniej przechowywane jako int.
supercat
2
Co sprawdziłem: te testy wzorcowe zawsze używały int („3”) jako czynnika lub operandu przypisania (wariant pętli, a następnie rzutowany). To, co zrobiłem, to użycie czynników typu / operandów przypisania w zależności od typu lwartości: int mult 76,481 ns int mult (typed) 72,581 ns short mult 87,908 ns short mult (typed) 90,772 ns byte mult 87,859 ns byte mult (typed) 89,524 ns int [] trav 88.905 ns int [] trav (wpisany) 89,126 ns short [] trav 10,563 ns short [] trav (typed) 10,039 ns byte [] trav 8,356 ns byte [] trav (wpisany) 8,338 ns Przypuszczam, że istnieje dużo niepotrzebnych rzutów. te testy zostały uruchomione na karcie Android.
Bondax
5

Używanie bytezamiast nich intmoże zwiększyć wydajność, jeśli używasz ich w dużej ilości. Oto eksperyment:

import java.lang.management.*;

public class SpeedTest {

/** Get CPU time in nanoseconds. */
public static long getCpuTime() {
    ThreadMXBean bean = ManagementFactory.getThreadMXBean();
    return bean.isCurrentThreadCpuTimeSupported() ? bean
            .getCurrentThreadCpuTime() : 0L;
}

public static void main(String[] args) {
    long durationTotal = 0;
    int numberOfTests=0;

    for (int j = 1; j < 51; j++) {
        long beforeTask = getCpuTime();
        // MEASURES THIS AREA------------------------------------------
        long x = 20000000;// 20 millions
        for (long i = 0; i < x; i++) {
                           TestClass s = new TestClass(); 

        }
        // MEASURES THIS AREA------------------------------------------
        long duration = getCpuTime() - beforeTask;
        System.out.println("TEST " + j + ": duration = " + duration + "ns = "
                + (int) duration / 1000000);
        durationTotal += duration;
        numberOfTests++;
    }
    double average = durationTotal/numberOfTests;
    System.out.println("-----------------------------------");
    System.out.println("Average Duration = " + average + " ns = "
            + (int)average / 1000000 +" ms (Approximately)");


}

}

Ta klasa testuje szybkość tworzenia nowego TestClass. Każdy test robi to 20 milionów razy i jest 50 testów.

Oto klasa TestClass:

 public class TestClass {
     int a1= 5;
     int a2= 5; 
     int a3= 5;
     int a4= 5; 
     int a5= 5;
     int a6= 5; 
     int a7= 5;
     int a8= 5; 
     int a9= 5;
     int a10= 5; 
     int a11= 5;
     int a12=5; 
     int a13= 5;
     int a14= 5; 
 }

Prowadziłem SpeedTestzajęcia i na koniec otrzymałem to:

 Average Duration = 8.9625E8 ns = 896 ms (Approximately)

Teraz zmieniam ints na bajty w TestClass i uruchamiam go ponownie. Oto wynik:

 Average Duration = 6.94375E8 ns = 694 ms (Approximately)

Wierzę, że ten eksperyment pokazuje, że jeśli instanujesz ogromną liczbę zmiennych, użycie bajtu zamiast int może zwiększyć wydajność

WVrock
źródło
4
Zauważ, że ten wzorzec mierzy tylko koszty związane z alokacją i budową i tylko w przypadku klasy z wieloma pojedynczymi polami. Jeśli operacje arytmetyczne / aktualizacyjne zostały wykonane na polach, wyniki @ meriton sugerują, że bytemoże to być >> wolniejsze << niż int.
Stephen C,
To prawda, powinienem lepiej to sformułować, żeby to wyjaśnić.
WVrock
2

Bajt jest ogólnie uważany za 8 bitów. krótki jest ogólnie uważany za 16 bitów.

W „czystym” środowisku, które nie jest java, tak jak wszystkie implementacje bajtów i długich znaków, krótkich znaków i innych zabawnych rzeczy są generalnie ukryte przed tobą, bajt lepiej wykorzystuje przestrzeń.

Jednak Twój komputer prawdopodobnie nie jest 8-bitowy i prawdopodobnie nie jest 16-bitowy. Oznacza to, że aby uzyskać w szczególności 16 lub 8 bitów, musiałby uciekać się do „oszustwa”, które marnuje czas, aby udawać, że ma możliwość dostępu do tych typów w razie potrzeby.

W tym momencie zależy to od sposobu implementacji sprzętu. Jednak sądziłem, że najlepszą prędkość uzyskuje się dzięki przechowywaniu rzeczy w kawałkach, które są wygodne w użyciu dla twojego procesora. 64-bitowy procesor lubi mieć do czynienia z elementami 64-bitowymi, a wszystko inne często wymaga „magii inżynierskiej”, aby udawać, że lubi się nimi zajmować.

Dmitry
źródło
3
Nie jestem pewien, co masz na myśli przez „magię inżynierską”… większość / wszystkie nowoczesne procesory mają szybkie instrukcje ładowania bajtu i rozszerzania go, zapisywania jednego z rejestru o pełnej szerokości i wykonywania bajtów o szerokości lub arytmetyka o małej szerokości w części rejestru o pełnej szerokości. Gdybyś miał rację, sensowne byłoby, o ile to możliwe, zastąpienie wszystkich wartości int na długich na 64-bitowym procesorze.
Ed Staub
Mogę sobie wyobrazić, że to prawda. Pamiętam tylko, że w symulatorze Motoroli 68k, którego używaliśmy, większość operacji mogła działać z wartościami 16-bitowymi, a nie z 32-bitowymi ani 64-bitowymi. Myślałem, że oznacza to, że systemy mają preferowany rozmiar wartości, który może optymalnie pobrać. Chociaż mogę sobie wyobrazić, że nowoczesne procesory 64-bitowe mogą z równą łatwością pobierać 8-bitowe, 16-bitowe, 32-bitowe i 64-bitowe, w tym przypadku jest to bez problemu. Dzięki za zwrócenie uwagi.
Dmitry,
„… jest ogólnie uważane za…” - Właściwie to jest jasno, jednoznacznie >> określone << jako te rozmiary. W Javie. A kontekstem tego pytania jest Java.
Stephen C
Duża liczba procesorów używa nawet tej samej liczby cykli do manipulowania i uzyskiwania dostępu do danych, które nie mają rozmiaru słowa, więc nie warto się tym martwić, chyba że mierzysz na konkretnej JVM i platformie.
drrob
Staram się mówić ogólnie. To powiedziawszy, nie jestem pewien co do standardu Javy w odniesieniu do rozmiaru bajtów, ale w tym momencie jestem całkiem przekonany, że jeśli jakikolwiek heretyk zdecyduje się na bajty inne niż 8-bitowe, Java nie będzie chciała ich dotykać pięciostopowym biegunem. Jednak niektóre procesory wymagają wyrównania wielobajtowego, a jeśli platforma Java je obsługuje, będzie musiała działać wolniej, aby dostosować się do tych mniejszych typów, lub magicznie przedstawić je z większymi reprezentacjami niż żądałeś. To zawsze preferuje int od innych typów, ponieważ zawsze używa ulubionego rozmiaru systemu.
Dmitry
2

Jednym z powodów, dla których krótkie / bajtowe / znaki są mniej wydajne, jest brak bezpośredniej obsługi tych typów danych. Oznacza to, że przez bezpośrednie wsparcie specyfikacje maszyny JVM nie wspominają o żadnym zestawie instrukcji dla tych typów danych. Instrukcje takie jak przechowywanie, ładowanie, dodawanie itp. Mają wersje dla typu danych int. Ale nie mają wersji dla short / byte / char. Np. Rozważ poniższy kod java:

void spin() {
 int i;
 for (i = 0; i < 100; i++) {
 ; // Loop body is empty
 }
}

To samo jest konwertowane na kod maszynowy, jak poniżej.

0 iconst_0 // Push int constant 0
1 istore_1 // Store into local variable 1 (i=0)
2 goto 8 // First time through don't increment
5 iinc 1 1 // Increment local variable 1 by 1 (i++)
8 iload_1 // Push local variable 1 (i)
9 bipush 100 // Push int constant 100
11 if_icmplt 5 // Compare and loop if less than (i < 100)
14 return // Return void when done

Teraz rozważ zmianę int na short, jak poniżej.

void sspin() {
 short i;
 for (i = 0; i < 100; i++) {
 ; // Loop body is empty
 }
}

Odpowiedni kod maszynowy zmieni się w następujący sposób:

0 iconst_0
1 istore_1
2 goto 10
5 iload_1 // The short is treated as though an int
6 iconst_1
7 iadd
8 i2s // Truncate int to short
9 istore_1
10 iload_1
11 bipush 100
13 if_icmplt 5
16 return

Jak widać, do manipulowania typem danych typu short nadal używa się instrukcji typu int data version i jawnie konwertuje int na short, gdy jest to wymagane. Teraz z tego powodu wydajność spada.

Teraz przytoczony powód, dla którego nie udzielono bezpośredniego wsparcia, jak następuje:

Wirtualna maszyna języka Java zapewnia najbardziej bezpośrednią obsługę danych typu int. Dzieje się tak częściowo w oczekiwaniu na wydajne implementacje stosów operandów wirtualnej maszyny języka Java i lokalnych tablic zmiennych. Motywuje to również częstość danych int w typowych programach. Inne typy integralne mają mniej bezpośredniego wsparcia. Na przykład nie ma bajtów, znaków ani krótkich wersji sklepu, ładowania lub dodawania instrukcji.

Cytat z specyfikacji JVM obecnej tutaj (strona 58).

Manish Bansal
źródło
Są to zdemontowane kody bajtowe; tj . instrukcje wirtualne JVM . Nie są optymalizowane przez javackompilator i nie można z nich wyciągnąć żadnych wiarygodnych wniosków na temat tego, jak program będzie działał w prawdziwym życiu. Kompilator JIT kompiluje te kody bajtowe do rzeczywistych natywnych instrukcji maszynowych i wykonuje dość poważną optymalizację procesu. Jeśli chcesz przeanalizować wydajność kodu, musisz sprawdzić instrukcje kodu natywnego. (I jest to skomplikowane, ponieważ musisz wziąć pod uwagę zachowanie czasowe wielostopniowego potoku x86_64.)
Stephen C
Uważam, że specyfikacje Java są przeznaczone do implementacji przez osoby wdrażające Java. Więc nie sądzę, żeby było więcej optymalizacji na tym poziomie. W każdym razie, ja też mogę się całkowicie mylić. Udostępnij link do odnośnika, aby poprzeć swoje oświadczenie.
Manish Bansal
Oto jeden fakt na poparcie mojego oświadczenia. Nie znajdziesz żadnych (wiarygodnych) danych dotyczących czasu, które mówią, ile cykli zegara zajmuje każda instrukcja kodu bajtowego maszyny JVM. Z pewnością nie jest publikowany przez Oracle ani innych dostawców JVM. Przeczytaj również stackoverflow.com/questions/1397009
Stephen C
Znalazłem stary (2008) artykuł, w którym ktoś próbował opracować niezależny od platformy model przewidywania wydajności sekwencji kodu bajtowego. Twierdzą, że ich przewidywania odbiegały o 25% w porównaniu z pomiarami RDTSC… na Pentium. I uruchomili maszynę JVM z wyłączoną kompilacją JIT! Źródła
Stephen C
Jestem tu po prostu zdezorientowany. Czy moja odpowiedź nie potwierdza faktów, które podałeś w sekcji ponownej wizyty?
Manish Bansal
0

Różnica jest ledwo zauważalna! To bardziej kwestia projektu, stosowności, jednolitości, przyzwyczajenia itp. Czasami jest to tylko kwestia gustu. Kiedy wszystko czego dbają o to, że program staje się gotowy do pracy i zastąpienie floatza intnie zaszkodzi poprawności, widzę żadnej przewagi w dzieje z jednego lub innego, chyba że można wykazać, że przy użyciu wydajności typu zmienia. Dostrajanie wydajności w oparciu o typy, które różnią się w 2 lub 3 bajtach, to naprawdę ostatnia rzecz, na której powinieneś się przejmować; Donald Knuth powiedział kiedyś: „Przedwczesna optymalizacja jest źródłem wszelkiego zła” (nie jestem pewien, czy to on, edytuj, jeśli znasz odpowiedź).

mrk
źródło
5
Nit: A float nie może reprezentować wszystkich liczb całkowitych an intcan; ani też nie może intreprezentować żadnej niecałkowitej wartości, która floatmoże. Oznacza to, że podczas gdy wszystkie wartości int są podzbiorem długich wartości, int nie jest podzbiorem liczby zmiennoprzecinkowej, a zmiennoprzecinkowa nie jest podzbiorem wartości typu int.
Oczekuję, że osoba odpowiadająca ma pisać substituting a float for a double, jeśli tak, odpowiadająca powinna edytować odpowiedź. Jeśli nie, odpowiadający powinien zwiesić głowę ze wstydu i wrócić do podstaw z powodów przedstawionych przez @pst oraz z wielu innych powodów.
Znak wysokiej wydajności
@HighPerformanceMark Nie, wstawiłem int i float, ponieważ właśnie o tym myślałem. Moja odpowiedź nie jest specyficzna dla Javy, chociaż myślałem C ... Ma być ogólna. Masz podły komentarz.
mrk