Java에서는 int 대신 byte 또는 short를 사용하고 double 대신 float를 사용하는 것이 더 효율적입니까?


91

나는 숫자가 아무리 작거나 커야 할지라도 항상 int와 doubles를 사용한다는 것을 알았습니다. 따라서 Java에서는 byte또는 short대신 intfloat대신 사용하는 것이 더 효율적 double입니까?

따라서 int와 double이 많은 프로그램이 있다고 가정하십시오. 숫자가 적합하다는 것을 알고 있다면 int를 바이트 또는 반바지로 변경하고 변경할 가치가 있습니까?

나는 자바에 서명되지 않은 유형이 없다는 것을 알고 있지만 숫자가 양수라는 것을 알았다면 할 수있는 추가 사항이 있습니까?

효율성이란 대부분 처리를 의미합니다. 모든 변수의 크기가 절반이고 계산도 다소 빠르면 가비지 수집기가 훨씬 빠르다고 가정합니다. (나는 안드로이드에서 일하고 있기 때문에 램에 대해서도 다소 걱정할 필요가 있다고 생각합니다)

(가비지 수집기는 원시가 아닌 객체 만 처리한다고 가정하지만 여전히 버려진 객체의 모든 원시를 삭제합니까?)

내가 가지고있는 작은 안드로이드 앱으로 시도했지만 전혀 차이를 느끼지 못했습니다. (나는 아무것도 "과학적으로"측정하지 않았지만.)

더 빠르고 효율적이어야한다고 생각하는 것이 잘못입니까? 내가 시간을 낭비했음을 알기 위해 대규모 프로그램에서 모든 것을 변경하고 싶지 않습니다.

새 프로젝트를 시작할 때 처음부터 할 가치가 있습니까? (내 말은 조금이라도 도움이 될 것이라고 생각하지만, 그렇다면 다시 한 번, 왜 누군가가 그렇게하는 것처럼 보이지 않는 것 같습니다.)

답변:


108

더 빠르고 효율적이어야한다고 생각하는 것이 잘못입니까? 내가 시간을 낭비했음을 알기 위해 대규모 프로그램에서 모든 것을 변경하고 싶지 않습니다.

짧은 답변

네, 틀 렸습니다. 대부분의 경우 사용 공간 측면에서 거의 차이없습니다 .

최적화가 필요하다는 명확한 증거가 없다면이를 최적화하는 것은 가치없습니다 . 특히 개체 필드의 메모리 사용을 최적화 해야하는 경우 다른 (더 효과적인) 조치를 취해야 할 것입니다.

더 긴 답변

Java Virtual Machine은 32 비트 기본 셀 크기의 배수 인 오프셋을 사용하여 스택 및 오브젝트 필드를 모델링합니다. 따라서 지역 변수 또는 객체 필드를 (예를 들어) a로 선언 byte하면 변수 / 필드가 int.

이에 대한 두 가지 예외가 있습니다.

  • longdouble값에는 2 개의 기본 32 비트 셀이 필요합니다.
  • 기본 유형의 배열은 압축 된 형식으로 표시되므로 (예를 들어) 바이트 배열은 32 비트 워드 당 4 바이트를 보유합니다.

따라서 및 ... 및 대규모 기본 요소 배열 사용을 최적화 할 가치 있습니다 . 그러나 일반적으로 아닙니다.longdouble

이론적으로, JIT는 수도 이를 최적화 할 수 있지만, 실제로 내가 수행하는 JIT 들어 본 적이없는. 한 가지 장애는 컴파일되는 클래스의 인스턴스가 생성 될 때까지 JIT가 일반적으로 실행될 수 없다는 것입니다. JIT가 메모리 레이아웃을 최적화했다면 동일한 클래스의 객체에 대해 두 개 (또는 그 이상)의 "향기"를 가질 수 있으며 이는 큰 어려움을 초래할 것입니다.


재검토

벤치 마크 결과 @meriton의 대답을 살펴보면 곱셈에 대한 성능 저하가 발생하는 대신 short및 사용하는 것으로 보입니다 . 실제로, 작업을 개별적으로 고려하면 벌금이 상당합니다. (단독으로 고려해서는 안되지만 ... 그것은 또 다른 주제입니다.)byteint

설명은 JIT가 각 경우에 32 비트 곱셈 명령을 사용하여 곱셈을 수행하고 있다는 것입니다. 그러나 byteshort경우 에는 중간 32 비트 값을 각 루프 반복에서 또는 로 변환하는 추가 명령을 실행 합니다. (이론적으로이 변환은 루프의 끝에서 한 번만 수행 할 수 있지만 최적화 프로그램이이를 파악할 수 있을지 의심 스럽습니다.)byteshort

어쨌든,이 전환의 또 다른 문제에 지점을 수행 short하고 byte최적화한다. 산술과 연산 집약적 인 알고리즘에서 성능을 악화 시킬 수 있습니다.


30
+1은 성능 문제에 대한 명확한 증거가 없으면 최적화하지 않습니다.
Bohemian

음, 왜 JVM은 JIT 컴파일이 클래스의 메모리 레이아웃을 압축 할 때까지 기다려야합니까? 필드 유형이 클래스 파일에 기록되기 때문에 JVM이 클래스로드시 메모리 레이아웃을 선택한 다음 필드 이름을 단어 오프셋이 아닌 바이트로 확인할 수 없었습니까?
meriton 2013-01-26

@meriton-객체 레이아웃 클래스로드 시간에 결정되며 그 이후에는 변경되지 않습니다. 내 대답의 "정밀 인쇄"부분을 참조하십시오. 코드가 JIT 될 때 실제 메모리 레이아웃이 변경되면 JVM이 처리하기가 정말 어려울 것입니다. (JIT 레이아웃을 최적화 할 수 있다고 말했을 때 , 그것은 가상적이고 비실용적입니다 ... JIT가 실제로 그것을 수행하는 것에 대해 들어 본 적이없는 이유를 설명 할 수 있습니다.)
Stephen C

알아. 객체가 생성되면 메모리 레이아웃을 변경하기 어렵지만 JVM이 그 전에, 즉 클래스로드 시간에 메모리 레이아웃을 최적화 할 수 있다는 점을 지적하려고했습니다. 다르게 말하면, JVM 사양이 단어 오프셋이있는 JVM의 동작을 설명한다고해서 JVM이 그런 방식으로 구현되어야한다는 것을 반드시 의미하지는 않습니다.
meriton

@meriton-JVM 사양은 로컬 프레임 / 객체 내에서 "가상 머신 단어 위반"에 대해 이야기하고 있습니다. 물리적 시스템 오프셋에 매핑되는 방법은 지정되지 않습니다. 실제로 하드웨어 별 필드 정렬 요구 사항이있을 수 있으므로이를 지정할 수 없습니다.
Stephen C

29

이는 JVM의 구현과 기본 하드웨어에 따라 다릅니다. 대부분의 최신 하드웨어는 메모리 (또는 첫 번째 레벨 캐시에서도)에서 단일 바이트를 가져 오지 않습니다. 즉, 더 작은 기본 유형을 사용한다고해서 일반적으로 메모리 대역폭 소비가 줄어들지 않습니다. 마찬가지로 최신 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
오히려 말보다 "배열을 사용하는 경우 특히"나는 말을 간단 할 수 있다고 생각 short하고 byte충분히 큰은 (효율성의 차이가 더 큰 배열 큰 문제가 할 수있는 배열에 저장 될 때 더 효율적 A는 byte[2]더있을 수 있습니다 또는보다 덜 효율적 int[2]이지만 어느 쪽이든 중요하지는 않지만) 개별 값은 int.
supercat 2013-01-26

2
내가 확인한 내용 : 이러한 벤치 마크는 항상 요소 또는 할당 피연산자로 int ( '3')를 사용했습니다 (루프 변형 후 캐스팅 됨). 내가 한 것은 lvalue 유형에 따라 유형이 지정된 인수 / 할당 피연산자를 사용하는 것입니다. int [] trav 88.905 ns int [] trav (입력 됨) 89.126 ns short [] trav 10.563 ns short [] trav (입력 됨) 10.039 ns byte [] trav 8.356 ns byte [] trav (입력 됨) 8.338 ns 많은 불필요한 캐스팅. 이 테스트는 안드로이드 탭에서 실행되었습니다.
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. 각 테스트는 2 천만 번 수행하며 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>> slower <<보다 int.
Stephen C

사실, 나는 그것을 명확히하기 위해 더 잘 말 했어야했다.
WVrock

2

일반적으로 바이트는 8 비트로 간주됩니다. short는 일반적으로 16 비트로 간주됩니다.

"순수한"환경에서, 모든 바이트와 long, shorts 및 기타 재미있는 것들이 일반적으로 숨겨져 있기 때문에 자바가 아니라면 byte는 공간을 더 잘 사용합니다.

그러나 컴퓨터는 아마도 8 비트가 아니고 16 비트도 아닐 것입니다. 이는 특히 16 비트 또는 8 비트를 얻으려면 필요할 때 이러한 유형에 액세스 할 수있는 것처럼 가장하기 위해 시간을 낭비하는 "속임수"에 의존해야 함을 의미합니다.

이 시점에서 하드웨어 구현 방법에 따라 다릅니다. 그러나 내가 힘들었던 것으로부터 최고의 속도는 CPU가 사용하기에 편안한 청크로 물건을 저장하는 것에서 달성됩니다. 64 비트 프로세서는 64 비트 요소를 처리하는 것을 좋아하며, 그보다 적은 것은 종종 처리하는 것을 좋아하는 척하기 위해 "엔지니어링 마법"이 필요합니다.


3
"엔지니어링 마법"이 무엇을 의미하는지 잘 모르겠습니다. 대부분의 / 모든 최신 프로세서에는 바이트를로드하고 부호 확장하고, 전폭 레지스터에서 하나를 저장하고, 바이트 너비를 수행하는 빠른 명령이 있습니다. 또는 전체 너비 레지스터의 일부에서 짧은 너비 산술. 당신이 옳았다면, 가능하다면 64 비트 프로세서에서 모든 정수를 long으로 바꾸는 것이 합리적 일 것입니다.
Ed Staub 2013 년

나는 그것이 사실이라고 상상할 수 있습니다. 우리가 사용한 Motorola 68k 시뮬레이터에서 대부분의 작업은 32 비트나 64 비트가 아닌 16 비트 값으로 작동 할 수 있다는 것을 기억합니다. 이것은 시스템이 최적으로 가져올 수있는 선호 값 크기를 가지고 있다는 것을 의미한다고 생각했습니다. 최신 64 비트 프로세서가 8 비트, 16 비트, 32 비트 및 64 비트를 똑같이 쉽게 가져올 수 있다고 상상할 수 있지만이 경우에는 문제가되지 않습니다. 지적 해 주셔서 감사합니다.
Dmitry

"...은 일반적으로 ...로 간주됩니다." -실제로는 해당 크기로 명확하고 명확하게 >> 지정 <<합니다. 자바에서. 이 질문의 맥락은 Java입니다.
Stephen C

많은 수의 프로세서가 동일한 수의주기를 사용하여 단어 크기가 아닌 데이터를 조작하고 액세스하기 때문에 특정 JVM 및 플랫폼에서 측정하지 않는 한 걱정할 필요가 없습니다.
drrob

나는 모든 일반적으로 말하려고 노력하고 있습니다. 그것은 바이트 크기와 관련하여 Java의 표준에 대해 실제로 확신하지 못한다고 말했지만,이 시점에서 이단자가 8 비트가 아닌 바이트를 결정하면 Java가 10 피트 극으로 그것들을 건 드리기를 원하지 않을 것이라고 꽤 확신합니다. 그러나 일부 프로세서는 멀티 바이트 정렬이 필요하며 Java 플랫폼에서 지원하는 경우 이러한 작은 유형을 처리하기 위해 더 느리게 작업을 수행하거나 요청한 것보다 더 큰 표현으로 마술처럼 표현해야합니다. 항상 시스템이 선호하는 크기를 사용하기 때문에 항상 다른 유형보다 int를 선호합니다.
Dmitry

2

short / byte / char의 성능이 떨어지는 이유 중 하나는 이러한 데이터 유형에 대한 직접적인 지원이 부족하기 때문입니다. 직접 지원한다는 것은 JVM 사양이 이러한 데이터 유형에 대한 명령 세트를 언급하지 않음을 의미합니다. 저장,로드, 추가 등과 같은 명령어에는 int 데이터 유형에 대한 버전이 있습니다. 그러나 short / byte / char 버전은 없습니다. 예를 들어 아래 자바 코드를 고려하십시오.

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

보시다시피 짧은 데이터 유형을 조작하기 위해 여전히 int 데이터 유형 명령어 버전을 사용하고 필요할 때 int를 short로 명시 적으로 변환합니다. 이로 인해 성능이 저하됩니다.

이제 직접 지원하지 않는 이유는 다음과 같습니다.

Java Virtual Machine은 int 유형의 데이터에 대한 가장 직접적인 지원을 제공합니다. 이것은 부분적으로 Java Virtual Machine의 피연산자 스택 및 지역 변수 배열의 효율적인 구현을 기대합니다. 또한 일반적인 프로그램에서 int 데이터의 빈도에 의해 동기가 부여됩니다. 다른 정수 유형은 직접적인 지원이 적습니다. 예를 들어, 저장,로드 또는 추가 명령어의 바이트, 문자 또는 짧은 버전이 없습니다.

여기에있는 JVM 사양에서 인용 (페이지 58).


이들은 분해 된 바이트 코드입니다. 즉, JVM 가상 지침. 그것들은 javac컴파일러에 의해 최적화 되지 않았으며, 프로그램이 실제 생활에서 어떻게 수행 될 것인지에 대해 신뢰할 수있는 추론을 그릴 수 없습니다. JIT 컴파일러는 이러한 바이트 코드를 실제 네이티브 머신 명령어 로 컴파일 하고 프로세스에서 매우 심각한 최적화를 수행합니다. 코드의 성능 을 분석 하려면 네이티브 코드 지침을 검토해야합니다. (다단계 x86_64 파이프 라인의 타이밍 동작을 고려해야하기 때문에 복잡합니다.)
Stephen C

Java 사양은 javac 구현자가 구현하는 것이라고 생각합니다. 그래서 저는 그 수준에서 더 이상 최적화가 이루어지지 않는다고 생각합니다. 어쨌든 나는 완전히 틀릴 수도 있습니다. 귀하의 진술을 뒷받침 할 참조 링크를 공유하십시오.
Manish Bansal

제 진술을 뒷받침하는 한 가지 사실이 있습니다. 각 JVM 바이트 코드 명령이 얼마나 많은 클럭 사이클을 사용하는지 알려주는 (신뢰할 수있는) 타이밍 수치를 찾을 수 없습니다. Oracle 또는 기타 JVM 공급 업체에서 확실히 게시하지 않았습니다. 또한, 읽기 stackoverflow.com/questions/1397009
스티븐 C

누군가 바이트 코드 시퀀스의 성능을 예측하기 위해 플랫폼 독립적 인 모델을 개발하려고 시도한 오래된 (2008) 논문을 찾았습니다. 그들은 그들의 예측이 펜티엄에서 RDTSC 측정에 비해 25 % 떨어 졌다고 주장합니다. 그리고 그들은 JIT 컴파일이 비활성화 된 상태에서 JVM을 실행하고있었습니다! 참조 : sciencedirect.com/science/article/pii/S1571066108004581
Stephen C

나는 여기서 혼란스러워합니다. 제 답변이 재검토 섹션에서 언급 한 사실을 뒷받침하지 않습니까?
Manish Bansal

0

그 차이는 거의 눈에 띄지 않습니다! 디자인, 적절성, 균일 성, 습관 등의 문제에 더 가깝습니다. 때로는 맛의 문제 일 수도 있습니다. 당신이 신경 쓰는 모든 것이 당신의 프로그램이 시작되고 실행되고 a float를으로 대체 int해도 정확성에 해를 끼치 지 않는 것이라면, 어느 유형을 사용하더라도 성능이 변한다는 것을 증명할 수 없다면 나는 둘 중 하나를 사용하는 데 아무런 이점이 없다고 생각합니다. 2 바이트 또는 3 바이트가 다른 유형을 기반으로 성능을 조정하는 것이 실제로 가장 중요합니다. Donald Knuth는 "조기 최적화는 모든 악의 근원입니다"라고 말했습니다 (그 사람인지 확실하지 않습니다. 답이 있으면 편집하십시오).


5
Nit : A float 는 캔의 모든 정수를 나타낼 수 없습니다int . 또한 할 수있는 int정수가 아닌 값을 float나타낼 수도 없습니다. 모든 INT 값 긴 값의 서브 세트 동안 즉, int로는 되지 플로트의 일부와 유동은 하지 의 int의 서브 세트.

나는 답변자가을 작성하려고 substituting a float for a double했기 때문에 답변자가 답변을 편집해야합니다. 응답자가 아니라면 부끄러워하고 @pst에 설명 된 이유와 다른 여러 이유로 기본으로 돌아 가야합니다.
High Performance Mark

@HighPerformanceMark 아니요 int와 float를 넣었습니다. 내 대답은 C ...라고 생각했지만 Java에만 국한되지 않습니다. 그것은 일반적입니다. 당신이 거기에있는 비열한 코멘트.
mrk
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.