Javaでは、intの代わりにbyteまたはshortを使用し、doubleの代わりにfloatを使用する方が効率的ですか?


91

私は、intとdoubleを常に使用していることに気付きました。それでJavaでは、byteまたはshort代わりにintfloat代わりに使用する方が効率的doubleですか?

したがって、intとdoubleがたくさんあるプログラムがあるとします。数値が収まるとわかっている場合、intをバイトまたはショートに変更してみる価値はありますか?

Javaに符号なしの型がないことはわかっていますが、数値が正の値のみになるとわかっている場合に何か他にできることはありますか?

効率的とは、主に処理を意味します。ガベージコレクターは、すべての変数のサイズが半分になるとはるかに速くなり、その計算もおそらく多少速くなると思います。(私はアンドロイドで作業しているので、ラムについても少し心配する必要があると思います)

(ガベージコレクターはオブジェクトのみを処理し、プリミティブは処理しないが、破棄されたオブジェクト内のすべてのプリミティブを削除すると思いますか?)

私が持っている小さなandroidアプリで試してみましたが、違いにまったく気づきませんでした。(私は「科学的に」何も測定しませんでしたが。)

それがより速く、より効率的であると仮定するのは間違っていますか?私は自分の時間を浪費していることを見つけるために大規模なプログラムですべてを変更して変更することを嫌います。

新しいプロジェクトを始めるとき、最初からやる価値はありますか?(私は少しでも役立つと思いますが、それでもそうなら、なぜ誰かがそうしているように思えないのですか?)

回答:


107

それがより速く、より効率的であると仮定するのは間違っていますか?私は自分の時間を浪費していることを見つけるために大規模なプログラムですべてを変更して変更することを嫌います。

短い答え

はい、あなたは間違っています。ほとんどの場合、使用されるスペースの点でほとんど違いがありません

最適化が必要であるという明確な証拠がない限り、これを最適化してみる価値はありません。あなたが行う場合と必要性を特にオブジェクトフィールドの最適化のメモリ使用量には、おそらく他の(より効果的な)対策を取る必要があります。

より長い答え

Java仮想マシンは、32ビットのプリミティブセルサイズの倍数であるオフセットを使用して、スタックとオブジェクトフィールドをモデル化します。したがって、ローカル変数またはオブジェクトフィールドを(たとえば)aとして宣言するbyteと、変数/フィールドはと同じように32ビットのセルに格納されintます。

これには2つの例外があります。

  • longそしてdouble値が2プリミティブ32ビット・セルを必要とします
  • プリミティブ型の配列はパックされた形式で表されるため、(たとえば)バイトの配列は32ビットワードあたり4バイトを保持します。

したがって、and ...およびプリミティブの大きな配列の使用を最適化する価値があるかもしれません。しかし、一般的には違います。longdouble

理論的には、JIT これを最適化できるかもしれませんが、実際には、最適化できるJITについて聞いたことがありません。障害の1つは、JITは通常、コンパイルされるクラスのインスタンスが作成されるまで実行できないことです。JITがメモリレイアウトを最適化した場合、同じクラスのオブジェクトの2つ(またはそれ以上)の「フレーバー」が存在する可能性があり、それは大きな困難をもたらします。


再訪

ベンチマークの結果を見ると、@ meritonの答えがわかります。乗算を使用するshortと、byte代わりに、使用するとintパフォーマンスが低下するようです。実際、操作を分離して考えると、ペナルティは大きくなります。(それらを個別に検討するべきではありません...しかし、それは別のトピックです。)

その理由は、JITはおそらく32ビットの乗算命令を使用して乗算を行っているということだと思います。しかしbyteandのshort場合、追加の命令を実行して、中間32ビット値をに変換するbyteshort、各ループの繰り返しで変換します。(理論的には、その変換はループの終わりに一度行うことができますが、オプティマイザがそれを理解できるとは思えません。)

とにかく、これは最適化への切り替えshortbyte最適化としての別の問題を示しています。それはパフォーマンスを悪化させる可能性があります...算術および計算集約型のアルゴリズムでは。


30
パフォーマンスの問題の明確な証拠がない限り、+ 1は最適化しない
ボヘミアン

えーっと、JVMはクラスのメモリレイアウトをパックするためにJITコンパイルを待つ必要があるのはなぜですか?フィールドのタイプはクラスファイルに書き込まれるため、JVMはクラスのロード時にメモリレイアウトを選択できず、フィールド名をワードオフセットではなくバイトとして解決できませんでしたか?
メリトン2013年

@meriton-オブジェクトのレイアウトクラスのロード時に決定され、その後は変更されないことは確かです。私の回答の「細字」の部分を参照してください。コードがJITされたときに実際のメモリレイアウトが変更された場合、JVMでの処理は非常に困難になります。(JIT レイアウトを最適化するかもしれないと言ったとき、それは仮想的で非現実的です...これが、JITが実際にそれを行っていることを聞いたことがない理由を説明する可能性があります。)
スティーブンC

知っている。オブジェクトが作成されるとメモリレイアウトを変更するのは難しいですが、JVMはその前、つまりクラスのロード時にメモリレイアウトを最適化する可能性があることを指摘しようとしました。言い換えれば、JVM仕様がワードオフセットを使用してJVMの動作を記述しているということは、JVMがそのように実装される必要があることを必ずしも意味するわけではありません。
メリトン2014

@meriton-JVM仕様は、ローカルフレーム/オブジェクト内の「仮想マシンワードオフセット」について話している。これらが物理マシンのオフセットにどのようにマッピングされるかは指定されていません。実際、ハードウェア固有のフィールドアライメント要件がある可能性があるため、指定することはできません。
スティーブンC

29

これは、JVMの実装と基盤となるハードウェアに依存します。最近のほとんどのハードウェアは、メモリから(または1次キャッシュからも)1バイトをフェッチしません。つまり、より小さいプリミティブタイプを使用しても、通常、メモリ帯域幅の消費量は減りません。同様に、最近のCPUのワードサイズは64ビットです。より少ないビットで操作を実行できますが、余分なビットを破棄することで機能します。

唯一の利点は、プリミティブ型が小さくなると、特に配列を使用する場合に、メモリレイアウトがよりコンパクトになることです。これによりメモリが節約され、参照の局所性が向上し(キャッシュミスの数が減る)、ガベージコレクションのオーバーヘッドが減少します。

ただし、一般的に言えば、小さいプリミティブタイプを使用する方が速くはありません。

これを実証するために、次のベンチマークを見てください。

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

私のやや古いノートに印刷されます(列を調整するためにスペースを追加します):

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

ご覧のとおり、パフォーマンスの違いはごくわずかです。アルゴリズムの最適化は、プリミティブタイプの選択よりもはるかに重要です。


3
むしろ言うよりも、「アレイを用いた場合に最も顕著」、私はと言うことは単純かもしれないと思うshortbyte十分な大きさが(効率差も大きい、アレイの大きな問題にはされている配列に格納する際、より効率的であり、byte[2]より多くのかもしれませんまたはより効率的ではありint[2]ませんが、どちらにしても十分ではありません)が、個々の値はとしてより効率的に格納されintます。
スーパーキャット2013年

2
私が確認したこと:これらのベンチマークでは、常に整数( '3')を因子または代入オペランド(ループバリアント、キャスト)として使用していました。lvalueタイプに応じて型付きファクター/代入オペランドを使用するために私がしたこと: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(typed)89.126 ns short [] trav 10.563 ns short [] trav(typed)10.039 ns byte [] trav 8.356 ns byte [] trav(typed)8.338 nsあると思います不要なキャストがたくさん。これらのテストはAndroidタブで実行されました。
Bondax

5

使用するbyte代わりに、intあなたは膨大な量でそれらを使用している場合は、パフォーマンスを向上させることができます。ここに実験があります:

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


}

}

このクラスは、新しいを作成する速度をテストしTestClassます。各テストは2000万回実行し、50のテストがあります。

これが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; 
 }

私はSpeedTestクラスを実行しました、そして結局これを得ました:

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

これで、TestClassでintをバイトに変更して、もう一度実行しています。結果は次のとおりです。

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

この実験は、大量の変数をインスタンス化する場合、intの代わりにbyteを使用すると効率が向上することを示していると思います


4
このベンチマークは、割り当てと構築に関連するコストのみを測定しており、個々のフィールドがたくさんあるクラスの場合のみであることに注意してください。フィールドで算術/更新操作が実行された場合、@ meritonの結果は、byteが>>より遅い<<の可能性があることを示唆していintます。
スティーブンC

確かに、私はそれを明確にするためにそれをよりよく言い表すべきでした。
WVrock

2

バイトは通常8ビットと見なされます。shortは一般に16ビットと見なされます。

「純粋な」環境では、バイトやロング、ショートのすべての実装がJavaであるわけではなく、その他の楽しいことは一般にユーザーから隠されているため、バイトはスペースをより有効に利用します。

ただし、コンピューターはおそらく8ビットではなく、おそらく16ビットではありません。これは、特に16ビットまたは8ビットを取得するには、必要なときにこれらのタイプにアクセスする能力を持っているふりをするために、時間を浪費する「トリッキー」に頼る必要があることを意味します。

この時点では、ハードウェアの実装方法によって異なります。しかし、私が教えてきたので、最高の速度は、CPUが使用するのに快適なものをチャンクに格納することによって達成されます。64ビットプロセッサは64ビットエレメントの処理が好きで、それより少ないものは、それらを処理するのが好きなふりをする「エンジニアリングマジック」を必要とすることがよくあります。


3
「エンジニアリングマジック」の意味がわかりません...ほとんどの/すべての最新のプロセッサは、バイトをロードして符号拡張する、全幅レジスタから1つを格納する、およびバイト幅を実行する高速命令を備えています全角レジスタの一部での短幅演算。もしあなたが正しかったなら、可能であれば、64ビットプロセッサですべてのintをlongに置き換えることは理にかなっています。
Ed Staub 2013年

それが本当だと想像できます。私たちが使用したMotorola 68kシミュレーターでは、ほとんどの操作が16ビット値で機能し、32ビットでも64ビットでも機能しなかったことを覚えています。これは、システムが最適にフェッチできる優先値サイズを持っていることを意味すると考えていました。最近の64ビットプロセッサは8ビット、16ビット、32ビット、64ビットを同じように簡単にフェッチできると想像できますが、この場合は問題ありません。ご指摘いただきありがとうございます。
ドミトリー

「...は一般的に...と見なされます」 -実際には、これらのサイズであることが明確に、明確に>>指定されています<<。Javaで。そして、この質問のコンテキストはJavaです。
スティーブンC

多数のプロセッサは、同じサイクル数を使用してワードサイズではないデータを操作およびアクセスするため、特定のJVMおよびプラットフォームで測定しない限り、心配する価値はありません。
drrob

私は一般的に言っていることを試みています。とは言っても、バイトサイズに関するJavaの標準については実際には定かではありませんが、現時点では、異端者が8ビット以外のバイトを決定した場合、Javaは10フィートの極でそれらに触れたくないと確信しています。ただし、一部のプロセッサはマルチバイトアライメントを必要とし、Javaプラットフォームがそれらをサポートしている場合、これらの小さい型の処理に対応するために処理を遅くするか、要求よりも大きい表現で魔法のように表現する必要があります。常にシステムのお気に入りのサイズを使用するため、他のタイプよりも常にintを優先します。
Dmitry

2

short / byte / charのパフォーマンスが低下する理由の1つは、これらのデータ型を直接サポートしていないことです。つまり、JVM仕様では、これらのデータ型の命令セットについては言及されていません。保存、ロード、追加などの命令には、intデータ型のバージョンがあります。ただし、short / byte / charのバージョンはありません。たとえば、以下のJavaコードを検討してください。

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

以下は、同じようにマシンコードに変換されます。

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

次のように、intをshortに変更することを検討してください。

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

対応するマシンコードは次のように変更されます。

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

ご覧のとおり、shortデータ型を操作するために、intデータ型の命令バージョンを使用し、必要に応じてintをshortに明示的に変換しています。これにより、パフォーマンスが低下します。

さて、次のように直接のサポートを与えない理由を挙げます。

Java仮想マシンは、int型のデータを最も直接サポートします。これは、Java仮想マシンのオペランドスタックとローカル変数配列の効率的な実装を見込んでいます。また、典型的なプログラムのintデータの頻度によっても動機付けられます。他の整数型では、直接サポートが少なくなります。たとえば、ストア、ロード、または追加命令のバイト、文字、またはショートバージョンはありません。

ここにある JVM仕様から引用(ページ58)。


これらは逆アセンブルされたバイトコードです。つまり、JVM 仮想命令。これらはjavacコンパイラーによって最適化されておらず、プログラムが実際にどのように機能するかについて信頼できる推論を引き出すことはできません。JITコンパイラーは、これらのバイトコードを実際のネイティブマシン命令にコンパイルし、プロセスでかなり深刻な最適化を行います。コードのパフォーマンスを分析する場合は、ネイティブコードの指示を調べる必要があります。(そして、マルチステージx86_64パイプラインのタイミング動作を考慮する必要があるため、複雑になります。)
Stephen C

Javaの仕様は、javacの実装者が実装するためのものだと思います。したがって、そのレベルで行われた最適化はこれ以上ないと思います。とにかく、私も完全に間違っている可能性があります。あなたの声明を支持するためにいくつかの参照リンクを共有してください。
Manish Bansal 2018年

さて、ここに私の声明を裏付ける1つの事実があります。各JVMバイトコード命令にかかるクロックサイクル数を示す(信頼できる)タイミング値はありません。確かにOracleや他のJVMサプライヤーからは公開されていません。また、stackoverflow.com
Stephen C

私は、誰かがバイトコードシーケンスのパフォーマンスを予測するためのプラットフォームに依存しないモデルを開発しようとした古い(2008)論文を見つけました。彼らは、ペンティアムでのRDTSC測定と比較して、予測が25%ずれていると主張しています。そして、JITコンパイルを無効にしてJVMを実行していました。参照:sciencedirect.com/science/article/pii/S1571066108004581
Stephen C

私はここで混乱しています。私の回答は、あなたが再考セクションで述べた事実を裏付けていませんか?
マニッシュBansal

0

違いはほとんど目立ちません!それは、デザイン、適切さ、均一性、習慣などの問題です。時々、それは単に好みの問題です。あなたが気にするのは、あなたのプログラムが立ち上がって実行され、正確さを損なうことのないものに置き換えてfloatも、intどちらかのタイプを使用することでパフォーマンスが変わることを実証できない限り、どちらか一方に進んでも利点はありません。2バイトまたは3バイトが異なるタイプに基づいてパフォーマンスを調整することは、本当に最後に注意する必要があります。Donald Knuthはかつて、「時期尚早な最適化がすべての悪の根源である」と述べました(それが彼であるかどうかはわかりません。答えがあれば編集してください)。


5
Nit:A float int缶のすべての整数を表すことはできません。また、がint表すことができる非整数値を表すこともできませんfloat。それはすべてのint値は長い値のサブセットであるが、整数である、ではないフロートのサブセットとフロートがない INTのサブセット。

私は、回答者がを書くことを意図していると予想しsubstituting a float for a doubleます。そうでない場合、回答者は@pstで概説されている理由や他の多くの理由により、恥ずかしくて頭を下げて基本に戻る必要があります。
ハイパフォーマンスマーク

@HighPerformanceMarkいいえintとfloatを考えました。私はCだと思っていましたが、私の答えはJavaに固有ではありません。あなたがそこに着いたという意味のコメント。
mrk 2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.