IT이야기

변수 선언 이전과 루프 내 간의 차이?

cyworld 2022. 5. 3. 21:28
반응형

변수 선언 이전과 루프 내 간의 차이?

나는 일반적으로 루프 내부에서 반복적으로 나타나는 것과 달리 루프 전에 버림 변수를 선언하는 것이 (성능) 차이를 만드는 것이 아닌가 항상 생각해 왔다.Java의 A(quite note) 예:

a) 루프 전 선언:

double intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

b) 내부 루프:

for(int i=0; i < 1000; i++){
    double intermediateResult = i;
    System.out.println(intermediateResult);
}

a와 b 중 어느 것이 더 나은가?

거듭되는 가변 선언(: b)은 이론상으로는 더 많은 오버헤드를 발생시키지만, 컴파일러는 충분히 영리해서 문제가 되지 않는다고 나는 의심한다.사례 b는 변수의 범위가 더 좁고, 변수의 범위가 사용되는 위치로 제한되는 장점이 있다.그래도 나는 예 a에 따라 코딩을 하는 편이야.

편집: 나는 특히 Java 케이스에 관심이 있다.

a와 b 중 어느 것이 더 나은가?

성능의 관점에서 보면, 그것을 측정해야 할 것이다. (그리고 내 생각에는 차이를 측정할 수 있다면 컴파일러는 별로 좋지 않다.)

유지관리의 관점에서 보면 b가 더 좋다.변수를 동일한 위치에서 가능한 가장 좁은 범위에서 선언하고 초기화하십시오.선언과 초기화 사이에 틈새 구멍을 남기지 말고, 필요 없는 네임스페이스를 오염시키지 마십시오.

네 A와 B를 각각 20번씩 반복해서 1억번씩 반복했어(JVM - 1.5.0)

A: 평균 실행 시간: 0.074초

B: 평균 실행 시간 : 0.067초

놀랍게도 B가 조금 더 빨랐다.컴퓨터처럼 빠른 속도로 이것을 정확하게 측정할 수 있는지 말하기가 어렵다.나도 A방식으로 코디하고 싶지만 별로 중요하지 않다고 말하고 싶다.

언어와 정확한 용도에 따라 다르다.예를 들어, C# 1에서는 아무런 차이가 없었다.C# 2에서는 익명의 방법(또는 C# 3의 람다 표현식)에 의해 로컬 변수가 캡처되는 경우 매우 현저한 차이를 만들 수 있다.

예:

using System;
using System.Collections.Generic;

class Test
{
    static void Main()
    {
        List<Action> actions = new List<Action>();

        int outer;
        for (int i=0; i < 10; i++)
        {
            outer = i;
            int inner = i;
            actions.Add(() => Console.WriteLine("Inner={0}, Outer={1}", inner, outer));
        }

        foreach (Action action in actions)
        {
            action();
        }
    }
}

출력:

Inner=0, Outer=9
Inner=1, Outer=9
Inner=2, Outer=9
Inner=3, Outer=9
Inner=4, Outer=9
Inner=5, Outer=9
Inner=6, Outer=9
Inner=7, Outer=9
Inner=8, Outer=9
Inner=9, Outer=9

차이점은 모든 동작이 동일한 것을 캡처한다는 것이다.outer변수, 그러나 각각은 고유한 개별적인inner가변의

다음은 내가 에 쓰고 편찬한 것이다.네트

double r0;
for (int i = 0; i < 1000; i++) {
    r0 = i*i;
    Console.WriteLine(r0);
}

for (int j = 0; j < 1000; j++) {
    double r1 = j*j;
    Console.WriteLine(r1);
}

이것이 내가 얻은 것이다.CIL이 다시 코드로 렌더링되는 경우 NET Reflector

for (int i = 0; i < 0x3e8; i++)
{
    double r0 = i * i;
    Console.WriteLine(r0);
}
for (int j = 0; j < 0x3e8; j++)
{
    double r1 = j * j;
    Console.WriteLine(r1);
}

그래서 편찬 후에는 둘 다 똑같이 보인다.관리되는 언어에서 코드는 CL/바이트 코드로 변환되고 실행 시 기계 언어로 변환된다.그래서 기계어에서는 스택에 더블이 생성되지 않을 수도 있다.단지 코드에서 에 대한 일시적인 변수라는 것을 반영하기 때문에 등록일 수도 있다.WriteLine를 위한 루프를 위한 최적화 규칙이 있다.그래서 보통 남자들은 그것에 대해 걱정하지 말아야 하는데, 특히 관리 언어에서는 더욱 그렇다.예를 들어, 예를 들어, 코드 관리를 최적화할 수 있는 경우, 예를 들어, 문자열을 여러 개 연결해야 하는 경우 등이 있다.string a; a+=anotherstring[i] 사용 vs 회사StringBuilder 다 에 큰 양쪽의 성능은 매우 큰 차이가 있다.컴파일러가 당신의 코드를 최적화하지 못하는 경우가 많은데, 그것은 그것이 더 큰 범위에서 의도된 것을 알아낼 수 없기 때문이다.하지만 그것은 당신을 위해 기본적인 것들을 최적화할 수 있다.

이건 VB에 나오는 고트카야.NET. Visual Basic 결과는 이 예에서 변수를 다시 초기화하지 않는다.

For i as Integer = 1 to 100
    Dim j as Integer
    Console.WriteLine(j)
    j = i
Next

' Output: 0 1 2 3 4...

을 출력할 할 때 이하하하면 0을 인쇄할 수 있다. (Visual Basic은 언시시시시시이이!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!).i그 후엔 매번

a를 추가하면= 0그러나 다음과 같은 결과를 기대할 수 있다.

For i as Integer = 1 to 100
    Dim j as Integer = 0
    Console.WriteLine(j)
    j = i
Next

'Output: 0 0 0 0 0...

나는 간단한 테스트를 했다:

int b;
for (int i = 0; i < 10; i++) {
    b = i;
}

for (int i = 0; i < 10; i++) {
    int b = i;
}

이 코드들을 gcc - 5.2.0으로 정리했다.그리고 이 두 코드의 메인()을 분해해 보니 그 결과가 다음과 같다.

1º:

   0x00000000004004b6 <+0>:     push   rbp
   0x00000000004004b7 <+1>:     mov    rbp,rsp
   0x00000000004004ba <+4>:     mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
   0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
   0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
   0x00000000004004d3 <+29>:    mov    eax,0x0
   0x00000000004004d8 <+34>:    pop    rbp
   0x00000000004004d9 <+35>:    ret

   0x00000000004004b6 <+0>: push   rbp
   0x00000000004004b7 <+1>: mov    rbp,rsp
   0x00000000004004ba <+4>: mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
   0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
   0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
   0x00000000004004d3 <+29>:    mov    eax,0x0
   0x00000000004004d8 <+34>:    pop    rbp
   0x00000000004004d9 <+35>:    ret 

그것은 같은 결과였다.두 코드가 같은 것을 생산한다는 증거가 아닌가?

언어에 의존한다 - IIRC C#는 이것을 최적화하기 때문에 아무런 차이가 없지만, 자바스크립트(예를 들어)는 매번 전체 메모리 할당 셰뱅을 할 것이다.

나는 항상 (컴파일러에 의존하지 않고) A를 사용하며 다음과 같이 다시 쓸 수도 있다.

for(int i=0, double intermediateResult=0; i<1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

이것은 여전히 제한된다.intermediateResult루프 범위까지 이동하지만 반복할 때마다 다시 덮지 않는다.

내 생각에는 b가 더 나은 구조라고 생각한다.a에서는 루프가 완료된 후 mederalResult의 마지막 값이 고정된다.

편집: 값 유형과 큰 차이가 없지만 참조 유형은 다소 무거울 수 있다.개인적으로, 나는 가능한 한 빨리 변수의 참조가 해제되는 것을 좋아하고, b는 당신을 위해 그렇게 한다.

나는 몇몇 컴파일러들이 둘 다 같은 코드로 최적화할 수 있을 것이라고 의심하지만, 확실히 전부는 아니다.그러니 전자와 함께 있는 게 좋을 거야.후자의 유일한 이유는 선언된 변수가 루프 에서만 사용되는지 확인하기 위해서입니다.

일반적으로 나는 내 변수를 가장 안쪽의 가능한 범위에서 선언한다.그래서, 만약 당신이 루프를 벗어나서 중간 결과물을 사용하지 않는다면, 나는 B와 함께 갈 것이다.

동료는 첫 번째 양식을 선호하며, 그것이 최적화라고 말하고, 선언문을 다시 사용하는 것을 선호한다.

나는 두 번째 것을 더 선호한다(그리고 내 동료를 설득하려고 노력한다!;-)

  • 변수의 범위를 필요한 곳으로 줄여주는 것이 좋은 일이다.
  • Java는 성능에서 큰 차이가 없을 정도로 최적화한다.IIRC, 아마도 두번째 형태는 훨씬 더 빠를 것이다.

어쨌든, 컴파일러 및/또는 JVM의 품질에 의존하는 조기 최적화의 범주에 속한다.

람다 등에서 변수를 사용하는 경우에는 C#에 차이가 있다.그러나 일반적으로 컴파일러는 변수가 루프 내에서만 사용된다고 가정할 때 기본적으로 동일한 작업을 수행한다.

기본적으로 동일하다는 점을 고려할 때:버전 b는 이 변수가 루프 후에 사용되지 않으며 사용할 수 없다는 것을 독자들에게 훨씬 더 분명하게 해준다는 점에 유의하십시오.또한 버전 b는 훨씬 쉽게 리팩터링된다. 버전 a에서는 루프 본체를 자체 방법으로 추출하는 것이 더 어렵다.또한 버전 b는 그러한 리팩터링에 부작용이 없음을 보장한다.

그래서, 버전은 나를 끝도 없이 짜증나게 한다. 왜냐하면 그것에는 이점이 없고 그것은 코드에 대해 추론하는 것을 훨씬 더 어렵게 만들기 때문이다.

음, 당신은 항상 그것을 위한 범위를 만들 수 있다.

{ //Or if(true) if the language doesn't support making scopes like this
    double intermediateResult;
    for (int i=0; i<1000; i++) {
        intermediateResult = i;
        System.out.println(intermediateResult);
    }
}

이런 식으로 변수를 한 번만 선언하면, 루프를 떠나면 그것은 죽게 된다.

나는 컴파일러에 따라 다르며 일반적인 대답을 하기가 어렵다고 생각한다.

나는 항상 당신이 당신의 루프 안에서 당신의 변수를 선언한다면 당신은 기억을 낭비하고 있다고 생각해왔다.이런 게 있으면:

for(;;) {
  Object o = new Object();
}

그러면 각 반복에 대해 객체를 생성해야 할 뿐만 아니라 각 객체에 대해 새로운 참조가 할당되어야 한다.쓰레기 수거기가 느리면 청소해야 할 참조 자료들이 널려 있는 것 같아.

단, 다음과 같은 사항이 있을 경우:

Object o;
for(;;) {
  o = new Object();
}

그런 다음 단일 참조를 만들고 매번 새 객체를 할당하는 것뿐입니다.물론, 범위를 벗어나는 데 시간이 좀 더 걸릴 수도 있지만, 그 다음엔 한 가지 참고 사항만 다루면 돼.

내 연습은 다음과 같다.

  • 변수의 유형이 단순하다면(int, double, ...) 변종 b(내부)를 선호한다.
    이유: 변수의 범위 축소.

  • 변수의 유형이 단순하지 않으면(어떤 종류의 또는 ) 변종 a(외부)를 선호한다.
    이유: ctor-dtor 통화 횟수 감소.

나는 아주 오랫동안 똑같은 질문을 받았다.그래서 나는 더 간단한 코드를 시험해 보았다.

결론:그러한 경우 성능 차이가 없다.

외부 루프 케이스

int intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i+2;
    System.out.println(intermediateResult);
}

내부 루프 케이스

for(int i=0; i < 1000; i++){
    int intermediateResult = i+2;
    System.out.println(intermediateResult);
}

IntelliJ의 디컴파일러에 있는 컴파일된 파일을 확인했는데, 두 경우 모두 동일한 파일을 받았다. Test.class

for(int i = 0; i < 1000; ++i) {
    int intermediateResult = i + 2;
    System.out.println(intermediateResult);
}

나는 또한 이 답변에서 주어진 방법을 사용하여 두 케이스에 대한 코드를 분해했다.답변과 관련된 부분만 보여주겠다.

외부 루프 케이스

Code:
  stack=2, locals=3, args_size=1
     0: iconst_0
     1: istore_2
     2: iload_2
     3: sipush        1000
     6: if_icmpge     26
     9: iload_2
    10: iconst_2
    11: iadd
    12: istore_1
    13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    16: iload_1
    17: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
    20: iinc          2, 1
    23: goto          2
    26: return
LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13      13     1 intermediateResult   I
            2      24     2     i   I
            0      27     0  args   [Ljava/lang/String;

내부 루프 케이스

Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: sipush        1000
         6: if_icmpge     26
         9: iload_1
        10: iconst_2
        11: iadd
        12: istore_2
        13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: iload_2
        17: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        20: iinc          1, 1
        23: goto          2
        26: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13       7     2 intermediateResult   I
            2      24     1     i   I
            0      27     0  args   [Ljava/lang/String;

세심한 주의를 기울인다면, 오직...Slot에 배정된.i그리고intermediateResultLocalVariableTable외모 순서의 상품으로 교환될 수 있다.슬롯의 동일한 차이는 코드의 다른 라인에 반영된다.

  • 추가 작업이 수행되지 않음
  • intermediateResult두 경우 모두 여전히 국지적 변수여서 접근 시간 차이가 없다.

보너스

컴파일러는 많은 최적화를 하는데, 이 경우 어떤 일이 일어나는지 살펴보십시오.

제로 워크 케이스

for(int i=0; i < 1000; i++){
    int intermediateResult = i;
    System.out.println(intermediateResult);
}

분해된 제로 작업

for(int i = 0; i < 1000; ++i) {
    System.out.println(i);
}

공연의 관점에서 보면, 밖이 더 좋다.

public static void outside() {
    double intermediateResult;
    for(int i=0; i < Integer.MAX_VALUE; i++){
        intermediateResult = i;
    }
}

public static void inside() {
    for(int i=0; i < Integer.MAX_VALUE; i++){
        double intermediateResult = i;
    }
}

나는 두 기능을 각각 10억 번 실행했다.아웃소싱은 65밀리초가 걸렸다.인사이드는 1.5초가 걸렸다.

관심 있는 사람이 있으면 노드 4.0.0으로 JS 테스트를 했다.루프 외부에 선언하면 평균 1,000회 이상의 시험에서 시험당 1억 회 반복으로 ~.5 ms의 성능 향상이 나타났다.그래서 나는 그것을 읽기/유지관리하기 가장 쉬운 방법으로 쓰라고 말할 것이다. B, imo.나는 내 코드를 바이올린에 넣겠지만, 나는 성능-현재의 노드 모듈을 사용했다.암호는 다음과 같다.

var now = require("../node_modules/performance-now")

// declare vars inside loop
function varInside(){
    for(var i = 0; i < 100000000; i++){
        var temp = i;
        var temp2 = i + 1;
        var temp3 = i + 2;
    }
}

// declare vars outside loop
function varOutside(){
    var temp;
    var temp2;
    var temp3;
    for(var i = 0; i < 100000000; i++){
        temp = i
        temp2 = i + 1
        temp3 = i + 2
    }
}

// for computing average execution times
var insideAvg = 0;
var outsideAvg = 0;

// run varInside a million times and average execution times
for(var i = 0; i < 1000; i++){
    var start = now()
    varInside()
    var end = now()
    insideAvg = (insideAvg + (end-start)) / 2
}

// run varOutside a million times and average execution times
for(var i = 0; i < 1000; i++){
    var start = now()
    varOutside()
    var end = now()
    outsideAvg = (outsideAvg + (end-start)) / 2
}

console.log('declared inside loop', insideAvg)
console.log('declared outside loop', outsideAvg)

A)는 B)보다 안전한 내기다.........만약 당신이 'int'나 'float'가 아닌 루프에서 구조를 초기화하고 있다면, 그러면 무엇이겠습니까?

맘에 들다

typedef struct loop_example{

JXTZ hi; // where JXTZ could be another type...say closed source lib 
         // you include in Makefile

}loop_example_struct;

//then....

int j = 0; // declare here or face c99 error if in loop - depends on compiler setting

for ( ;j++; )
{
   loop_example loop_object; // guess the result in memory heap?
}

기억력 유출로 인한 문제에 직면하게 될 것이 분명하다!그러므로 나는 'A'가 더 안전한 반면 'B'는 가까운 소스 라이브러리에 작용하는 메모리 축적 esp에 취약하다고 믿는다.Linux에서 특별히 하위 툴인 'Helgrind'에서 'Valgrind' Tool을 확인하십시오.

재미있는 질문이다.내 경험으로 볼 때 코드를 위해 이 문제에 대해 토론할 때 고려해야 할 궁극적인 질문이 있다.

변수가 글로벌해야 하는 이유가 있는가?

코드를 구성하는데 더 좋고 코드 라인이 더 적게 필요하기 때문에 변수를 여러 번 국지적으로 선언하는 것과 달리 전 세계적으로 한 번만 선언하는 것이 타당하다.단, 한 가지 방법 내에서 국지적으로만 선언하면 된다면, 변수가 그 방법에만 전적으로 관련성이 있다는 것이 분명하도록 나는 그 방법으로 초기화할 것이다.이 변수를 후자 옵션을 선택할 경우 초기화되는 방법 이외의 다른 방법으로 호출하지 않도록 주의하십시오. - 코드는 현재 말한 내용을 알지 못하며 오류를 보고할 것이다.

또한, 참고로, 다른 방법들 간에 지역 변수 이름을 복제하지 마십시오. 목적이 거의 동일하더라도 혼란스러울 뿐이지요.

이것이 더 나은 형태다.

double intermediateResult;
int i = byte.MinValue;

for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}

1) 이러한 방식으로 두 변수를 모두 한 번 선언하고 각 변수를 사이클에 대해 선언하지 않는다.2) 다른 모든 선택사항보다 더 뚱뚱한 과제3) 따라서 모범 사례 규칙은 반복 이외의 선언이다.

Go에서도 동일한 작업을 시도하고, 다음 작업을 사용하여 컴파일러 출력을 비교go tool compile -S1.9.4로

조립기 출력에 따른 영차.

루프를 종료한 후 변수의 내용을 보고 싶을 때 (A)를 사용한다.디버깅할 때만 문제가 된다.코드 한 줄을 저장하기 때문에 코드를 좀 더 컴팩트하게 하고 싶을 때 (B)를 사용한다.

내 컴파일러가 충분히 똑똑하다는 것을 안다고 해도, 나는 그것에 의존하는 것을 좋아하지 않을 것이고, a) 변종을 사용할 것이다.

b) 변종은 루프 본체 후에 반드시 중간 결과물을 사용할 수 없게 해야 하는 경우에만 내게 의미가 있다.하지만 어쨌든 이렇게 절박한 상황은 상상조차 할 수 없다.......

편집: Jon Sket은 루프 내부의 가변 선언이 실제 의미 차이를 만들 수 있다는 것을 보여주면서 매우 좋은 지적을 했다.

참조URL: https://stackoverflow.com/questions/407255/difference-between-declaring-variables-before-or-in-loop

반응형