programing

(변수1 % 변수2 == 0) 비효율적인 이유는?

prostudy 2022. 5. 7. 09:32
반응형

(변수1 % 변수2 == 0) 비효율적인 이유는?

나는 자바에 처음 와본 사람인데, 어젯밤에 코드 몇 개를 틀었는데, 정말 신경 쓰였어.모든 X 출력을 루프에 표시하기 위해 간단한 프로그램을 만들고 있었는데, MUX의 성능 저하가 눈에 띄었는데, 그때 나는 계수를 로 사용했다.variable % variablevariable % 5000아니면 뭐랄까.이게 왜 그런 건지, 원인이 뭔지 누가 설명해 줄 수 있어?그래서 내가 더 나아질 수 있도록...

여기 "효율적" 코드가 있다(구문 좀 틀리면 미안하다. 지금 코드가 있는 컴퓨터에 없다).

long startNum = 0;
long stopNum = 1000000000L;

for (long i = startNum; i <= stopNum; i++){
    if (i % 50000 == 0) {
        System.out.println(i);
    }
}

여기 "효율적이지 않은 코드"가 있다.

long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 50000;

for (long i = startNum; i <= stopNum; i++){
    if (i % progressCheck == 0) {
        System.out.println(i);
    }
}

내가 차이점을 측정할 날짜 변수가 있었는데, 일단 충분히 길어지면, 첫 번째 변수는 50ms, 다른 하나는 12초나 그 비슷한 시간이 걸렸다.당신은 아마도 더 많은 것을 해야 할 것이다.stopNum또는 감소시키다progressCheck만약 당신의 PC가 내 것보다 더 효율적이거나 그렇지 않다면.

웹에서 이 질문을 찾아보았지만 답을 찾을 수가 없어, 어쩌면 내가 제대로 묻지 않는 것일지도 몰라.

편집: 내 질문이 그렇게 인기가 있을 줄은 몰랐는데, 모든 답변에 감사해.나는 각 반을 벤치마킹하는 시간을 가졌고 비효율적인 코드는 1/4초 대 10초 정도의 시간이 걸렸다.만약 그들이 인쇄물을 사용하고 있다면, 하지만 둘 다 같은 양을 사용하고 있기 때문에, 나는 특히 그 불일치가 반복될 수 있기 때문에, 그것이 크게 왜곡될 것이라고 생각하지 않는다.답에 대해서는 자바에 처음이라 일단 어떤 답이 최선인지 투표로 결정하도록 하겠다.수요일까지 하나 고르도록 노력하겠다.

편집2: 오늘 밤 또 다른 테스트를 할 겁니다. 여기서 계량 대신 변수를 증가시키고, 진행률 확인에 도달하면 변수 하나를 수행한 다음, 세 번째 옵션에 대해 변수를 0으로 재설정하는 겁니다.

EDIT3.5:

이 코드를 사용했고, 아래에 내 결과를 보여줄게.멋진 도움에 모두들 고마워!나는 또한 긴 것의 짧은 값을 0과 비교하려고 노력했다. 그래서 나의 모든 새로운 수표는 반복적으로 같은 "65536"번 반복해서 일어난다.

public class Main {


    public static void main(String[] args) {

        long startNum = 0;
        long stopNum = 1000000000L;
        long progressCheck = 65536;
        final long finalProgressCheck = 50000;
        long date;

        // using a fixed value
        date = System.currentTimeMillis();
        for (long i = startNum; i <= stopNum; i++) {
            if (i % 65536 == 0) {
                System.out.println(i);
            }
        }
        long final1 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        //using a variable
        for (long i = startNum; i <= stopNum; i++) {
            if (i % progressCheck == 0) {
                System.out.println(i);
            }
        }
        long final2 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();

        // using a final declared variable
        for (long i = startNum; i <= stopNum; i++) {
            if (i % finalProgressCheck == 0) {
                System.out.println(i);
            }
        }
        long final3 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        // using increments to determine progressCheck
        int increment = 0;
        for (long i = startNum; i <= stopNum; i++) {
            if (increment == 65536) {
                System.out.println(i);
                increment = 0;
            }
            increment++;

        }

        //using a short conversion
        long final4 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        for (long i = startNum; i <= stopNum; i++) {
            if ((short)i == 0) {
                System.out.println(i);
            }
        }
        long final5 = System.currentTimeMillis() - date;

                System.out.println(
                "\nfixed = " + final1 + " ms " + "\nvariable = " + final2 + " ms " + "\nfinal variable = " + final3 + " ms " + "\nincrement = " + final4 + " ms" + "\nShort Conversion = " + final5 + " ms");
    }
}

결과:

  • 고정 = 874ms(약 1000ms이지만, 2의 검정력 때문에 더 빠름)
  • 변수 = 8590ms
  • 최종 변수 = 1944ms(500 사용 시 ~1000ms)
  • 증분 = 1904 ms
  • 짧은 변환 = 679ms

별로 놀랍지도 않은 것은, 분할의 부족으로 인해, 쇼트 컨버전스는 "빠른" 방식보다 23% 더 빨랐다.이것은 주목하기에 흥미롭다.매 256회(또는 그 부근)마다 어떤 것을 표시하거나 비교해야 할 경우, 이렇게 할 수 있으며,

if ((byte)integer == 0) {'Perform progress check code here'}

최종 관심 사항 중 하나는 65536(예쁜 숫자가 아님)으로 "최종 신고 변수"에 계수를 사용한 것이 고정 값보다 (낮음)의 절반 속도였다.이전에도 거의 같은 속도로 벤치마킹하고 있었다.

OSR(스택 교체) 스텁을 측정하는 경우.

OSR 스텁은 메서드가 실행되는 동안 해석 모드에서 컴파일된 코드로 실행을 전송하기 위해 특별히 고안된 컴파일된 메서드의 특수 버전이다.

OSR 스텁은 해석된 프레임과 호환되는 프레임 레이아웃이 필요하기 때문에 일반 방법만큼 최적화되지 않는다.나는 이미 다음 대답에서 이것을 보여주었다: 1, 2, 3

이곳에서도 비슷한 일이 일어난다.이지 않은 긴 이 에 있는 효율적이지 않은 코드'가 긴 루프를 실행하는 동안, 이 방법은 루프 바로 안쪽에 있는 스택 교체용으로 특별히 컴파일된다.상태가 해석된 프레임에서 OSR 컴파일된 방법으로 전송되며, 이 상태는 다음을 포함한다.progressCheck지역 변수이 시점에서 JIT는 변수를 상수로 대체할 수 없으므로 강도 감소와 같은 특정 최적화를 적용할 수 없다.

특히 이것은 JIT가 정수분할곱셈으로 대체하지 않는다는 것을 의미한다.(GCC가 정수 분할을 구현할 이상한 숫자의 곱셈을 사용하는 이유는?를 참조하십시오.미리 보기 컴파일러의 ASM 트릭에 대해, 이 값이 인라이닝/상수 간격띄우기 후 컴파일 시간 상수인 경우, 그러한 최적화가 활성화된 경우.의 정수 리터럴 권한%표현식은 또한 에 의해 최적화된다.gcc -O0, J에 의해 최적화되는 곳과 유사함OSR 스텁에서도 ITer).

그러나 동일한 방법을 여러 번 실행하면 두 번째 실행과 이후의 실행은 완전히 최적화된 정규(비 OSR) 코드를 실행하게 된다.다음은 이론을 증명하기 위한 벤치마크(JMH를 사용하여 벤치마크):

@State(Scope.Benchmark)
public class Div {

    @Benchmark
    public void divConst(Blackhole blackhole) {
        long startNum = 0;
        long stopNum = 100000000L;

        for (long i = startNum; i <= stopNum; i++) {
            if (i % 50000 == 0) {
                blackhole.consume(i);
            }
        }
    }

    @Benchmark
    public void divVar(Blackhole blackhole) {
        long startNum = 0;
        long stopNum = 100000000L;
        long progressCheck = 50000;

        for (long i = startNum; i <= stopNum; i++) {
            if (i % progressCheck == 0) {
                blackhole.consume(i);
            }
        }
    }
}

그리고 그 결과는 다음과 같다.

# Benchmark: bench.Div.divConst

# Run progress: 0,00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration   1: 126,967 ms/op
# Warmup Iteration   2: 105,660 ms/op
# Warmup Iteration   3: 106,205 ms/op
Iteration   1: 105,620 ms/op
Iteration   2: 105,789 ms/op
Iteration   3: 105,915 ms/op
Iteration   4: 105,629 ms/op
Iteration   5: 105,632 ms/op


# Benchmark: bench.Div.divVar

# Run progress: 50,00% complete, ETA 00:00:09
# Fork: 1 of 1
# Warmup Iteration   1: 844,708 ms/op          <-- much slower!
# Warmup Iteration   2: 105,893 ms/op          <-- as fast as divConst
# Warmup Iteration   3: 105,601 ms/op
Iteration   1: 105,570 ms/op
Iteration   2: 105,475 ms/op
Iteration   3: 105,702 ms/op
Iteration   4: 105,535 ms/op
Iteration   5: 105,766 ms/op

의 첫 번째 반복divVar비효율적으로 컴파일된 OSR 스텁 때문에 실제로 훨씬 느리다.그러나 이 방법이 처음부터 다시 실행되자마자, 이용 가능한 컴파일러 최적화를 모두 활용하는 새로운 제한되지 않은 버전이 실행된다.

@clfruff 코멘트 후속으로 JIT에서1 생성된 코드를 확인했는데 그 결과는 다음과 같다.

을 위해variable % 5000(상수별 분할):

mov     rax,29f16b11c6d1e109h
imul    rbx
mov     r10,rbx
sar     r10,3fh
sar     rdx,0dh
sub     rdx,r10
imul    r10,rdx,0c350h    ; <-- imul
mov     r11,rbx
sub     r11,r10
test    r11,r11
jne     1d707ad14a0h

을 위해variable % variable:

mov     rax,r14
mov     rdx,8000000000000000h
cmp     rax,rdx
jne     22ccce218edh
xor     edx,edx
cmp     rbx,0ffffffffffffffffh
je      22ccce218f2h
cqo
idiv    rax,rbx           ; <-- idiv
test    rdx,rdx
jne     22ccce218c0h

분할은 항상 곱셈보다 오래 걸리기 때문에 마지막 코드 조각은 성능이 떨어진다.

Java 버전:

java version "11" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11+28)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode)

1 - 사용된 VM 옵션:-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/java/Main.main

다른 사람들이 지적했듯이 일반적인 계량 운영은 분업을 해야 한다.경우에 따라서는 구획으로 ( 컴파일러에 의해) 구획을 대체할 수 있다.그러나 두 가지 모두 덧셈/굴절과 비교하면 느릴 수 있다.따라서 최상의 성능은 다음과 같은 선에서 기대할 수 있다.

long progressCheck = 50000;

long counter = progressCheck;

for (long i = startNum; i <= stopNum; i++){
    if (--counter == 0) {
        System.out.println(i);
        counter = progressCheck;
    }
}

(경미한 옵티메이징 시도로서, 여기서는 다음과 비교되는 많은 아키텍처에서 감소 전 다운-카운터(down-counter)를 사용한다.0ALU의 플래그는 이미 선행 연산에 의해 적절히 설정되었기 때문에 산술 연산 직후에 정확히 0 지침/CPU 사이클이 소요된다.그러나 적절한 최적화 컴파일러는 당신이 글을 쓰더라도 자동으로 최적화 작업을 수행한다.if (counter++ == 50000) { ... counter = 0; }.)

루프 카운터(Roop Counter)를 알고 있기 때문에 종종 여러분이 정말로 계수를 원하지 않거나 필요로 하지 않는다는 것을 알아두십시오.i 어떤 이든 단지 했을 뿐이고,에게 줄 잔량에 단지 를 볼 또는 어떤 것이든 단지 1씩 증가했을 뿐이고, 여러분은 실제로 계수가 여러분에게 줄 나머지 실제 잔량은 신경 쓰지 않는다. 단지 증분별 카운터가 어떤 값에 맞는지 보기만 하면 된다.

또 다른 '트릭'은 예를 들어, 두 개의 값/한계의 힘을 사용하는 것이다.progressCheck = 1024;. 비트를 통해 2의 검정력을 신속하게 계산할 수 있는 계수and, 즉.if ( (i & (1024-1)) == 0 ) {...}이 또한 상당히 빨라야 하며, 일부 아키텍처에서는 명시적 수준을 능가할 수 있다.counter상부의

위 코드의 성능을 보고 나도 놀랐어.그것은 모두 컴파일러가 선언된 변수에 따라 프로그램을 실행하는 데 걸리는 시간에 관한 것이다.두 번째(효율적이지 않음) 예:

for (long i = startNum; i <= stopNum; i++) {
    if (i % progressCheck == 0) {
        System.out.println(i)
    }
}

두 변수 사이의 계량 연산을 수행하고 있는 경우.여기서 컴파일러는 다음 값을 확인해야 한다.stopNum그리고progressCheck변수이고 값이 변경될 수 있기 때문에 반복할 때마다 이러한 변수에 대해 위치한 특정 메모리 블록으로 이동한다.

그래서 각 반복 컴파일러가 메모리 위치로 가서 변수의 최신 값을 확인한 것이다.따라서 컴파일 시간에 컴파일러는 효율적인 바이트 코드를 만들 수 없었다.

첫 번째 코드 예제에서는 실행 내에서 변경되지 않는 변수와 상수 숫자 값 사이에서 계량 연산자를 수행하며, 컴파일러는 메모리 위치에서 해당 숫자 값의 값을 확인할 필요가 없다.컴파일러가 효율적인 바이트 코드를 만들 수 있었던 이유다.신고하면progressCheck로서final또는 하나의final static가변 시간/시간/시간 변경 시 컴파일러는 최종 변수임을 알고 그 값이 변경되지 않을 경우 컴파일러를 대체한다.progressCheck와 함께50000코드:

for (long i = startNum; i <= stopNum; i++) {
    if (i % 50000== 0) {
        System.out.println(i)
    }
}

이제 여러분은 이 코드가 첫 번째 (효율적인) 코드 예시처럼 보인다는 것을 알 수 있다.우리가 위에서 언급했듯이 첫 번째 코드의 성능은 효율적으로 작동할 것이다.두 코드 사례의 실행 시간에는 큰 차이가 없을 것이다.

참조URL: https://stackoverflow.com/questions/54405842/why-is-if-variable1-variable2-0-inefficient

반응형