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, byte
czy short
zamiast int
i float
zamiast 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ł.)
źródło
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.
źródło
short
ibyte
są 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; abyte[2]
może być więcej lub mniej wydajne niż anint[2]
, ale nie na tyle, aby mieć znaczenie w obu przypadkach), ale te indywidualne wartości są wydajniej przechowywane jakoint
.Używanie
byte
zamiast nichint
moż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
SpeedTest
zaję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ść
źródło
byte
może to być >> wolniejsze << niżint
.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ć.
źródło
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:
Cytat z specyfikacji JVM obecnej tutaj (strona 58).
źródło
javac
kompilator 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.)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
float
zaint
nie 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ź).źródło
float
nie może reprezentować wszystkich liczb całkowitych anint
can; ani też nie możeint
reprezentować żadnej niecałkowitej wartości, którafloat
moż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.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.