Java'da, int ve float yerine double yerine byte veya short kullanmak daha mı verimli?


91

Sayı ne kadar küçük veya büyük olursa olsun her zaman int ve double kullandığımı fark ettim. Java Yani, kullanmak daha verimlidir byteveya shortyerine intve floatyerine double?

Öyleyse, bol miktarda ints ve double içeren bir programım olduğunu varsayalım. Sayının uygun olacağını bilseydim, derinliklerimi bayt veya kısa olarak değiştirmeye değer mi?

Java'nın işaretsiz türleri olmadığını biliyorum, ancak sayının yalnızca pozitif olacağını bilseydim yapabileceğim fazladan bir şey var mı?

Verimli derken çoğunlukla işlemeyi kastediyorum. Tüm değişkenler yarım boyutta olsaydı ve hesaplamaların da muhtemelen biraz daha hızlı olacağını düşünürdüm, çöp toplayıcı çok daha hızlı olurdu. (Sanırım android üzerinde çalıştığım için ram konusunda da biraz endişelenmem gerekiyor)

(Çöp toplayıcının yalnızca Nesnelerle uğraştığını ve ilkel olmadığını, ancak yine de terk edilmiş nesnelerdeki tüm ilkelleri sildiğini varsayıyorum, değil mi?)

Sahip olduğum küçük bir android uygulamasıyla denedim ama gerçekten bir fark görmedim. (Hiçbir şeyi "bilimsel olarak" ölçmemiş olsam da.)

Daha hızlı ve daha verimli olması gerektiğini varsaymakta yanlış mıyım? Zamanımı boşa harcadığımı anlamak için büyük bir programda her şeyi değiştirmekten nefret ederim.

Yeni bir projeye başladığımda baştan yapmaya değer mi? (Demek istediğim, her küçük parçanın yardımcı olacağını düşünüyorum ama öyleyse yine neden kimse yapmıyor gibi görünüyor?)

Yanıtlar:


110

Daha hızlı ve daha verimli olması gerektiğini varsaymakta yanlış mıyım? Zamanımı boşa harcadığımı anlamak için büyük bir programda her şeyi değiştirmekten nefret ederim.

Kısa cevap

Evet yanılıyorsunuz. Çoğu durumda, kullanılan alan açısından çok az fark yaratır.

Öyle değmez optimizasyon gerekli olduğunu açık kanıtlar olmadığı sürece ... Bu duruma getirmek için çalışıyoruz. Eğer yoksa Ve gerek özellikle nesne alanlarının optimize bellek kullanımına, muhtemelen diğer (daha etkili) önlemler almak gerekir.

Daha uzun cevap

Java Sanal Makinesi, 32 bitlik ilkel hücre boyutunun (gerçekte) katları olan ofsetleri kullanarak yığınları ve nesne alanlarını modeller. Dolayısıyla, yerel bir değişkeni veya nesne alanını (diyelim ki) a olarak ilan ettiğinizde byte, değişken / alan, tıpkı int.

Bunun iki istisnası vardır:

  • longve doubledeğerler 2 ilkel 32 bit hücre gerektirir
  • İlkel tür dizileri, (örneğin) bir bayt dizisi 32 bit kelime başına 4 bayt tutacak şekilde paketlenmiş biçimde temsil edilir.

Bu yüzden ve ... ve büyük ilkel dizilerin kullanımını optimize etmeye değer olabilir . Ama genel olarak hayır.longdouble

Teorik olarak, bir JIT olabilir bu optimize edebilmek, ama pratikte ben yapan bir JIT duymadım. Bir engel, JIT'in tipik olarak, derlenen sınıfın örnekleri yaratılıncaya kadar çalışamayacağıdır. JIT bellek düzenini optimize ettiyse, aynı sınıftaki nesnenin iki (veya daha fazla) "çeşidine" sahip olabilirsiniz ... ve bu büyük zorluklara neden olur.


Yeniden ziyaret

@ Meriton'ın cevabındaki kıyaslama sonuçlarına bakıldığında, çarpma için shortve byteyerine kullanmak intbir performans cezasına neden olur. Nitekim, operasyonları tek başına ele alırsanız, ceza önemlidir. (Onları tek başına düşünmemelisiniz ... ama bu başka bir konu.)

Sanırım açıklama, JIT'in muhtemelen çarpmaları her durumda 32bit çarpma talimatlarını kullanarak yapıyor olmasıdır. Ancak byteve shortdurumunda, ara 32 bit değerini a veya her döngü yinelemesinde dönüştürmek için ekstra talimatlar yürütür . (Teoride, bu dönüşüm döngünün sonunda bir kez yapılabilir ... ancak optimize edicinin bunu çözebileceğinden şüpheliyim.)byteshort

Neyse, bu geçiş ile başka soruna noktayı yapar shortve bytebir optimizasyon olarak. Aritmetik ve yoğun işlem gerektiren bir algoritmada performansı daha da kötüleştirebilir .


30
Bir performans sorunu olduğuna dair net kanıtınız yoksa +1 optimizasyon yapmaz
Bohemian

Eee, neden JVM bir sınıfın bellek düzenini paketlemek için JIT derlemesini beklemek zorunda? Alan türleri sınıf dosyasına yazıldığı için, JVM sınıf yükleme zamanında bir bellek düzeni seçip sonra alan adlarını sözcük ofsetleri yerine bayt olarak çözümleyemez mi?
meriton

@meriton - Emin nesne düzenleri olduğunu değilim edilmektedir sınıf yükleme süresi belirlenir ve bunlar bundan sonra değişmez. Cevabımın "ince yazılı" kısmına bakın. Kod JIT'lendiğinde gerçek bellek düzenleri değiştiyse, JVM'nin uğraşması gerçekten zor olacaktır. (JIT'in düzeni optimize edebileceğini söylediğimde , bu varsayımsal ve pratik değildir ... bu, bir JIT'in bunu yaptığını neden hiç duymadığımı açıklayabilir.)
Stephen C

Biliyorum. Nesneler oluşturulduktan sonra bellek düzenlerinin değiştirilmesi zor olsa da, bir JVM'nin bundan önce, yani sınıf yükleme zamanında bellek düzenini yine de optimize edebileceğini belirtmeye çalışıyordum. Farklı bir şekilde ifade edersek, JVM spesifikasyonunun bir JVM'nin davranışını kelime kaydırmalarıyla tanımlaması, bir JVM'nin bu şekilde uygulanması gerektiği anlamına gelmez - büyük olasılıkla öyle olsa da.
meriton

@meriton - JVM spesifikasyonu, yerel çerçeveler / nesneler içindeki "sanal makine kelime uzaklıklarından" bahsediyor. Bunların fiziksel makine ofsetleriyle nasıl eşleştirildiği BELİRTİLMEMİŞTİR. Aslında, donanıma özel alan hizalama gereksinimleri olabileceğinden bunu belirleyemez.
Stephen C

29

Bu, JVM'nin uygulanmasına ve temeldeki donanıma bağlıdır. Çoğu modern donanım, tek baytları bellekten (veya hatta birinci seviye önbellekten) almayacaktır, yani daha küçük ilkel türlerin kullanılması genellikle bellek bant genişliği tüketimini azaltmaz. Aynı şekilde, modern CPU'nun kelime boyutu 64 bittir. İşlemleri daha az bit üzerinde gerçekleştirebilirler, ancak bu da daha hızlı olmayan ekstra bitleri atarak çalışır.

Tek faydası, daha küçük ilkel türlerin, özellikle dizileri kullanırken, daha kompakt bir bellek düzeniyle sonuçlanabilmesidir. Bu, bellekten tasarruf sağlayarak referansın yerelliğini iyileştirebilir (böylece önbellek kayıplarının sayısını azaltabilir) ve çöp toplama ek yükünü azaltabilir.

Ancak genel olarak konuşursak, daha küçük ilkel türleri kullanmak daha hızlı değildir.

Bunu göstermek için aşağıdaki karşılaştırmaya bakın:

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);
        }
    }
}

biraz eski defterime yazdırılan (sütunları ayarlamak için boşluk ekleyerek):

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

Gördüğünüz gibi, performans farklılıkları oldukça küçük. Algoritmaları optimize etmek, ilkel tür seçiminden çok daha önemlidir.


3
"En çok dizileri kullanırken" demek yerine, önemli olacak kadar büyük dizilerde depolandığında daha basit shortve bytedaha verimli olacağını düşünüyorum (dizi ne kadar büyükse, verimlilik farkı o kadar büyük byte[2]olabilir ; a daha fazla olabilir veya bir'den daha az verimli int[2], ancak her iki şekilde de önemli olacak kadar değil), ancak bireysel değerler olarak daha verimli bir şekilde saklanır int.
supercat

2
Kontrol ettiğim şey: Bu ölçütler faktör veya atama işleneni olarak her zaman bir int ('3') kullandı (döngü değişkeni, sonra çevrildi). Yaptığım şey, lvalue türüne bağlı olarak tiplenmiş faktörleri / atama işlenenlerini kullanmaktı: 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 (yazılan) 89.126 ns kısa [] trav 10.563 ns kısa [] trav (yazılan) 10.039 ns bayt [] çapraz 8.356 ns bayt [] trav (yazılan) 8.338 ns Sanırım bir çok fazla gereksiz döküm. bu test bir android sekmesinde yapıldı.
Bondax

5

byteBunun yerine kullanmak int, büyük miktarda kullanıyorsanız performansı artırabilir. İşte bir deney:

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)");


}

}

Bu sınıf, yeni bir TestClass. Her test bunu 20 milyon kez yapıyor ve 50 test var.

İşte 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; 
 }

SpeedTestSınıfı yönetiyorum ve sonunda şunu anladım:

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

Şimdi TestClass'ta ints'i bayta dönüştürüp tekrar çalıştırıyorum. İşte sonuç:

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

İnanıyorum ki bu deney, çok miktarda değişken örnekliyorsanız, int yerine bayt kullanmanın verimliliği artırabileceğini


4
Bu kıyaslamanın yalnızca tahsis ve inşaatla ilgili maliyetleri ölçtüğünü ve yalnızca çok sayıda bireysel alana sahip bir sınıf durumunda olduğunu unutmayın. Alanlar üzerinde aritmetik / güncelleme işlemleri gerçekleştirildiyse, @ meriton sonuçları bytebunun >> daha yavaş << olduğunu gösterir int.
Stephen C

Doğru, açıklığa kavuşturmak için daha iyi ifade etmeliydim.
WVrock

2

bayt genellikle 8 bit olarak kabul edilir. kısa, genellikle 16 bit olarak kabul edilir.

Baytların, uzunların ve kısa ve diğer eğlenceli şeylerin tüm uygulamaları gibi java olmayan "saf" bir ortamda, bayt, alanı daha iyi kullanır.

Bununla birlikte, bilgisayarınız muhtemelen 8 bit değildir ve muhtemelen 16 bit değildir. bu, özellikle 16 veya 8 bit elde etmek için, gerektiğinde bu türlere erişme yeteneğine sahipmiş gibi davranmak için zaman harcayan "hileye" başvurması gerektiği anlamına gelir.

Bu noktada, donanımın nasıl uygulandığına bağlıdır. Bununla birlikte, öğrendiğim kadarıyla, en iyi hız, CPU'nuzun kullanımı için rahat olan şeyleri yığınlar halinde depolayarak elde edilir. 64 bitlik bir işlemci, 64 bit öğelerle uğraşmaktan hoşlanır ve bundan daha azı, onlarla uğraşmayı seviyormuş gibi davranmak için genellikle "mühendislik sihri" gerektirir.


3
"Mühendislik büyüsü" ile ne demek istediğinizden emin değilim ... çoğu / tüm modern işlemciler, bir bayt yüklemek ve onu işaretleyerek genişletmek, tam genişlikli bir kayıttan birini depolamak ve bayt genişliği yapmak için hızlı talimatlara sahiptir. veya tam genişlikli bir yazmacın bir bölümünde kısa genişlikli aritmetik. Eğer haklı olsaydınız, mümkün olduğu durumlarda 64 bitlik bir işlemcide tüm girişleri long'larla değiştirmek mantıklı olurdu.
Ed Staub

Bunun doğru olduğunu hayal edebiliyorum. Kullandığımız Motorola 68k simülatöründe çoğu işlemin 32 bit veya 64 bit olmasa da 16 bit değerlerle çalıştığını hatırlıyorum. Bunun, sistemlerin en iyi şekilde getirebileceği tercih edilen bir değer boyutuna sahip olduğu anlamına geldiğini düşünüyordum. Modern 64bit işlemcilerin 8bit, 16 bit, 32bit ve 64bit'i eşit kolaylıkla getirebileceğini hayal edebiliyorum, bu durumda sorun değil. Bunu belirttiğiniz için teşekkürler.
Dmitry

"... genellikle ... olarak kabul edilir." - Aslında, açıkça, açık bir şekilde >> bu boyutlar olarak belirtilir <<. Java'da. Ve bu sorunun bağlamı Java'dır.
Stephen C

Hatta çok sayıda işlemci, kelime boyutunda olmayan verileri işlemek ve bunlara erişmek için aynı sayıda döngü kullanır, bu nedenle belirli bir JVM ve platformda ölçüm yapmadığınız sürece gerçekten endişelenmeye değmez.
drrob

Genel olarak söylemeye çalışıyorum. Bununla birlikte, Java'nın bayt boyutuyla ilgili standardından aslında emin olmadığımı söyledi, ancak bu noktada, herhangi bir sapkın 8 bitlik olmayan bayta karar verirse, Java'nın onlara on fitlik bir kutupla dokunmak istemeyeceğine ikna oldum. Bununla birlikte, bazı işlemciler çok baytlı hizalamaya ihtiyaç duyar ve eğer Java platformu bunları destekliyorsa, bu küçük türlerle başa çıkmak için işleri daha yavaş yapması veya bunları sizin istediğinizden daha büyük temsillerle sihirli bir şekilde temsil etmesi gerekecektir. Her zaman sistemin favori boyutunu kullandığından, her zaman int'i diğer türlere tercih eder.
Dmitry

2

Short / byte / char'ın daha az performanslı olmasının nedenlerinden biri, bu veri türleri için doğrudan destek eksikliğidir. Doğrudan destekle, JVM spesifikasyonlarının bu veri türleri için herhangi bir komut setinden bahsetmediği anlamına gelir. Depola, yükle, ekle vb. Talimatların int veri türü için sürümleri vardır. Ancak kısa / bayt / karakter için sürümleri yoktur. Örneğin, aşağıdaki java kodunu düşünün:

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

Aynısı aşağıdaki gibi makine koduna dönüştürülür.

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

Şimdi, aşağıdaki gibi int'i kısa olarak değiştirmeyi düşünün.

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

İlgili makine kodu aşağıdaki şekilde değişecektir:

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

Gördüğünüz gibi, kısa veri tipini değiştirmek için, hala int veri tipi komut versiyonunu kullanıyor ve gerektiğinde int'i açıkça kısaca dönüştürüyor. Artık bundan dolayı performans düşüyor.

Şimdi, doğrudan destek vermemenin gerekçesi şu şekildedir:

Java Sanal Makinesi, int türündeki veriler için en doğrudan desteği sağlar. Bu kısmen Java Sanal Makinesi'nin işlenen yığınlarının ve yerel değişken dizilerinin verimli uygulamalarının öngörülmesidir. Ayrıca tipik programlarda int verilerinin sıklığı ile motive edilir. Diğer integral türleri daha az doğrudan desteğe sahiptir. Örneğin, mağaza, yükleme veya ekleme talimatlarının bayt, karakter veya kısa sürümleri yoktur.

Burada bulunan JVM spesifikasyonundan alıntılanmıştır (Sayfa 58).


Bunlar, ayrıştırılmış bayt kodlardır; yani JVM sanal talimatları. javacDerleyici tarafından optimize edilmezler ve programın gerçek hayatta nasıl performans göstereceği konusunda onlardan güvenilir bir çıkarım yapamazsınız. JIT derleyicisi bu bayt kodlarını gerçek yerel makine yönergelerine derler ve süreçte oldukça ciddi optimizasyonlar yapar. Kodun performansını analiz etmek istiyorsanız , yerel kod talimatlarını incelemeniz gerekir. (Ve karmaşıktır çünkü çok aşamalı bir x86_64 ardışık düzeninin zamanlama davranışını hesaba katmanız gerekir.)
Stephen C

Java spesifikasyonlarının javac uygulayıcılarının uygulaması için olduğuna inanıyorum. Bu nedenle, bu seviyede daha fazla optimizasyon yapıldığını düşünmüyorum. Her neyse, ben de tamamen yanılıyor olabilirim. Lütfen ifadenizi desteklemek için bazı referans bağlantılarını paylaşın.
Manish Bansal

İşte ifademi destekleyen bir gerçek var. Her JVM bayt kodu talimatının kaç saat döngüsü aldığını size söyleyen (güvenilir) zamanlama rakamları bulamazsınız. Oracle veya diğer JVM tedarikçileri tarafından kesinlikle yayınlanmamaktadır. Ayrıca stackoverflow.com/questions/1397009'u
Stephen C

Birinin bayt kodu dizilerinin performansını tahmin etmek için platformdan bağımsız bir model geliştirmeye çalıştığı eski (2008) bir makale buldum. Bir Pentium'daki RDTSC ölçümlerine kıyasla tahminlerinin% 25 oranında yanlış olduğunu iddia ediyorlar. Ve JVM'yi JIT derlemesi devre dışı bırakılarak çalıştırıyorlardı! Referans: sciencedirect.com/science/article/pii/S1571066108004581
Stephen C

Burada kafam karıştı. Cevabım, yeniden ziyaret bölümünde belirttiğiniz gerçekleri desteklemiyor mu?
Manish Bansal

0

Fark neredeyse hiç fark edilmiyor! Daha çok bir tasarım, uygunluk, tekdüzelik, alışkanlık vb. Meseleler ... Bazen bu sadece bir zevk meselesi. Önemsediğiniz tüm program ve koşma ve bir ikame ayağa kalkar o olduğunda floatbir için intdoğruluğunu zarar vermeyecek, ben biri için gidiş herhangi bir avantaj ya da başka sen kanıtlayamazlarsa bu da tip değişiklikler gösterirse performansını kullanarak. Performansın 2 veya 3 bayt cinsinden farklı türlere göre ayarlanması gerçekten önemsemeniz gereken son şeydir; Donald Knuth bir keresinde şöyle demişti: "Erken optimizasyon tüm kötülüklerin köküdür" (onun olduğundan emin değilim, cevabınız varsa düzenleyin).


5
Nit: A bir float kutunun tüm tam sayılarını temsil intedemez; ne de inttam sayı olmayan herhangi bir değeri temsil floatedemez. Diğer bir deyişle, tüm int değerleri uzun değerlerin bir alt kümesi iken, bir int, bir float'ın alt kümesi değildir ve bir float, bir int'in alt kümesi değildir .

Cevaplayanın yazması substituting a float for a doublegerektiğini düşünüyorum, eğer öyleyse cevaplayanın cevabı düzenlemesi gerekir. Cevap vermediyse, utanç içinde kafa tutmalı ve @pst tarafından özetlenen nedenlerden ve diğer birçok nedenden dolayı temellere geri dönmelidir.
Yüksek Performans Mark

@HighPerformanceMark Hayır, int ve float koydum çünkü düşündüğüm buydu. Cevabım Java'ya özgü değil ama C ... Genel olması gerekiyordu. Orada aldığın ortalama yorum.
mrk
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.