数値がどれだけ小さくても大きくても、常にintとdoubleを使用していることに気付きました。 Javaでは、byte
の代わりにshort
またはint
とfloat
の代わりにdouble
を使用する方が効率的ですか?
だから、たくさんの整数と倍精度を持つプログラムがあると仮定します。数値が収まることがわかっていれば、intをバイトまたはショートに変更して、変更する価値はありますか?
Javaには符号なしの型はありませんが、数値が正の値のみであることがわかっている場合にできることはありますか?
効率的というのは、ほとんど処理を意味します。すべての変数のサイズが半分になると、ガベージコレクターの速度がはるかに速くなり、おそらく計算も多少速くなると思います。 (私はAndroidに取り組んでいるので、RAMについても多少心配する必要があります)
(ガベージコレクターはプリミティブではなくオブジェクトのみを処理しますが、放棄されたオブジェクトのすべてのプリミティブを削除すると仮定しますか?)
小さなAndroidのアプリを試しましたが、実際には違いに気付きませんでした。ただし、「科学的に」測定しませんでした。)
それがより速く、より効率的であると仮定するのは間違っていますか?私は自分の時間を無駄にしたことを知るために、大規模なプログラムのすべてを変更して変更するのは嫌です。
新しいプロジェクトを始めるとき、最初からやる価値はありますか? (つまり、少しでも助けになると思いますが、もしそうなら、なぜ誰かがそれをしているように見えないのですか?)
それがより速く、より効率的であると仮定するのは間違っていますか?私は自分の時間を無駄にしたことを知るために、大規模なプログラムのすべてを変更して変更するのは嫌です。
はい、あなたは間違っています。ほとんどの場合、使用されるスペースに関して小さな差になります。
価値がないこれを最適化しようとしています...最適化が必要であるという明確な証拠がない限り。また、特にオブジェクトフィールドのメモリ使用量を最適化するためにneedを行う場合は、おそらく他の(より効果的な)対策を講じる必要があります。
Java Virtual Machineは、32ビットのプリミティブセルサイズの(実質的に)倍数であるオフセットを使用して、スタックおよびオブジェクトフィールドをモデル化します。したがって、ローカル変数またはオブジェクトフィールドを(たとえば) byte
、変数/フィールドはint
と同様に32ビットセルに格納されます。
これには2つの例外があります。
long
およびdouble
値には、2つのプリミティブな32ビットセルが必要ですしたがって、mightlong
およびdouble
...およびプリミティブの大きな配列の使用を最適化する価値があります。しかし、一般的にはありません。
理論的には、JIT mightでこれを最適化できますが、実際には、これを行うJITについて聞いたことがありません。 1つの障害は、通常、コンパイルされるクラスのインスタンスが作成されるまでJITを実行できないことです。 JITがメモリレイアウトを最適化した場合、同じクラスのオブジェクトの2つ(またはそれ以上)の「フレーバー」を持つことができます...これは大きな困難をもたらします。
@meritonの回答のベンチマーク結果を見ると、short
の代わりにbyte
とint
を使用すると、乗算のパフォーマンスが低下するようです。実際、操作を単独で検討する場合、ペナルティは重大です。 (すべきではありません...しかし、それは別の問題です。)
説明は、JITがおそらく各ケースで32ビット乗算命令を使用して乗算を行っていることだと思います。ただし、byte
およびshort
の場合、extra命令を実行して、各ループで中間32ビット値をbyte
またはshort
に変換します繰り返し。 (理論的には、その変換はループの終わりに一度行うことができますが、オプティマイザーがそれを理解できるとは思いません。)
とにかく、これは、最適化としてshort
およびbyte
に切り替えることに関する別の問題を示しています。算術演算および計算集約的なアルゴリズムでは、パフォーマンスを悪化させる可能性がありますさらに悪い ...
それは、基盤となるハードウェアだけでなく、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
ご覧のとおり、パフォーマンスの違いはごくわずかです。アルゴリズムの最適化は、プリミティブ型の選択よりもはるかに重要です。
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を使用すると効率が向上することを示していると思います
通常、バイトは8ビットと見なされます。 shortは一般に16ビットと見なされます。
Javaではない「純粋な」環境では、バイトとロング、ショート、およびその他の楽しいことのすべての実装が一般に隠されているため、バイトはスペースをより有効に活用します。
ただし、コンピューターはおそらく8ビットではなく、おそらく16ビットではありません。つまり、特に16または8ビットを取得するには、必要なときにこれらのタイプにアクセスできるように見せかけるために、時間を浪費する「トリケリー」に頼る必要があります。
この時点で、ハードウェアの実装方法に依存します。しかし、私が考えてきたことから、最高の速度は、CPUが快適に使用できるチャンクに格納することで達成されます。 64ビットプロセッサは64ビット要素を扱うのが好きであり、それよりも小さいものを扱うのが好きなふりをするために「エンジニアリングマジック」が必要になることがよくあります。
Short/byte/charのパフォーマンスが低い理由の1つは、これらのデータ型を直接サポートしていないことです。直接サポートとは、JVM仕様ではこれらのデータ型の命令セットに言及していないことを意味します。ストア、ロード、追加などの命令には、intデータ型のバージョンがあります。ただし、short/byte/charのバージョンはありません。例えば。以下を考慮してくださいJava code:
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仕様から引用 here (ページ58)。
違いはほとんど目立ちません!それは、デザイン、適切さ、均一性、習慣などの問題です。時には、単に好みの問題です。プログラムが起動して実行され、float
をint
に置き換えても正確性が損なわれないということだけが気になる場合、それを実証できない限り、どちらに行ってもメリットはありません。いずれかのタイプを使用すると、パフォーマンスが変わります。 2バイトまたは3バイトが異なるタイプに基づいてパフォーマンスをチューニングすることは、実際に最後に注意すべきことです。ドナルド・クヌースはかつて言った:「時期尚早な最適化はすべての悪の根源だ」(それが彼であるかどうかわからない、答えがあれば編集する)。