접미사 증가 연산자 피


25

성능상의 이유로 (일부 경우) 접미사 증가 연산자를 피해야 한다는 것을 읽었습니다 .

그러나 이것이 코드 가독성에 영향을 미치지 않습니까? 내 의견으로는 :

for(int i = 0; i < 42; i++);
    /* i will never equal 42! */

다음보다 낫습니다.

for(int i = 0; i < 42; ++i);
    /* i will never equal 42! */

그러나 이것은 아마도 습관이 아닐 것입니다. 분명히, 나는 많은 사용을 보지 못했다 ++i.

이 경우 가독성을 떨어 뜨리기 어려운 성능입니까? 아니면 나는 단지 장님이고, ++i더 읽기 쉽다 i++?


1
i++성능에 영향을 줄 수 있다는 것을 알기 전에 사용 ++i했으므로 전환했습니다. 처음에는 후자가 조금 이상하게 보였지만 조금 후에 익숙해 져서 자연 스럽습니다 i++.
gablin

15
++i그리고 i++특정 상황에서 다른 일을, 그들은 동일한 가정하지 않습니다.
Orbling

2
이것이 C 또는 C ++에 관한 것입니까? 그들은 매우 다른 두 언어입니다! :-) C ++에서 관용적 for-loop는 for (type i = 0; i != 42; ++i)입니다. operator++오버로드 될 수 있을 뿐만 아니라 operator!=operator<. 접두사 증분은 접두사보다 비싸지 않으며 같지 않습니다. 우리는 어느 것을 사용해야합니까?
보 퍼슨

7
++ C라고 부르지 않아야합니까?
Armand

21
@Stephen : C ++는 C를 가져 와서 추가 한 다음 이전 것을 사용 한다는 의미 입니다.
supercat

답변:


58

사실 :

  1. i ++와 ++ i는 똑같이 읽기 쉽습니다. 익숙하지 않기 때문에 마음에 들지 않지만 본질적으로 잘못 해석 할 수있는 것은 없으므로 더 이상 읽거나 쓰는 작업이 없습니다.

  2. 적어도 어떤 경우에는 접미사 연산자의 효율성이 떨어집니다.

  3. 그러나 99.99 %의 경우 (a) 어쨌든 단순 또는 기본 유형으로 작동하고 큰 오브젝트를 복사하는 경우에만 문제가되기 때문에 중요하지 않습니다. (b) 성능에 있지 않습니다. 컴파일러가 코드를 최적화할지 여부를 모르는 코드의 중요한 부분 (c), 그렇지 않을 수도 있습니다.

  4. 따라서 접두사를 특별히 필요로하지 않는 한 접두사를 사용하는 것이 좋습니다. 왜냐하면 (a) 다른 것들과 정확하게 일치시키는 것이 좋은 습관이고 (b) 푸른 달에 한 번 접두사를 사용하려고하기 때문입니다. 당신이 의미하는 것을 항상 쓰면, 그 가능성은 줄어 듭니다. 성능과 최적화 사이에는 항상 상충 관계가 있습니다.

상식을 사용해야하며 필요할 때까지 미세 최적화하지 말아야합니다. 일반적으로 이것은 다음을 의미합니다. 첫째, 시간이 중요하지 않은 코드 (일반적으로 500MB 객체를 이유없이 값으로 전달하는 것과 같은 기본 개념 오류를 나타내는 것)에서도 수용 할 수없는 비효율적 인 코드 구성을 배제합니다. 둘째, 코드를 작성하는 다른 모든 방법 중에서 가장 명확한 방법을 선택하십시오.

그러나 여기서는 대답이 간단하다고 생각합니다. 접두어를 구체적으로 필요로하지 않는 한 접두사를 작성하는 것이 (a) 매우 명확하고 (b) 훨씬 더 효율적일 가능성이 높기 때문에 항상 기본적으로 작성해야하지만 잊어 버리더라도 걱정하지 마십시오.

6 개월 전, 나는 당신과 똑같다고 생각했는데, i ++는 더 자연 스럽지만, 그것은 당신이 익숙한 것입니다.

편집 1 : 일반적 으로이 일을 신뢰하는 "더 효과적인 C ++"의 Scott Meyers는 일반적으로 사용자 정의 유형에 postfix 연산자를 사용하지 말아야한다고 말합니다 (postfix 증가 함수의 유일한 구현은 개체를 복사 할 경우 접두사 증가 함수를 호출하여 증가를 수행하고 복사를 반환하지만 복사 작업은 비용이 많이들 수 있습니다).

따라서 우리는 (a) 오늘날의 사실인지, (b) 고유 한 유형에도 적용되는지 (적은지) (c) "++"를 사용해야하는지에 대한 일반적인 규칙이 있는지 여부를 모릅니다. 가벼운 반복자 클래스 이상의 것. 그러나 위에서 설명한 모든 이유로 인해 이전에 말한 것을 중요하지 않습니다.

편집 2 : 이것은 일반적인 관행을 나타냅니다. 특정 인스턴스에서 중요하다고 생각되면 프로파일 링하여 확인해야합니다. 프로파일 링은 쉽고 저렴하며 작동합니다. 첫 번째 원칙에서 최적화해야 할 것을 추론하는 것은 어렵고 비싸며 작동하지 않습니다.


귀하의 게시물에 돈이 있습니다. aClassInst = someOtherClassInst + yetAnotherClassInst ++와 같이 접두사 + 연산자 및 증가 후 ++가 오버로드 된 표현식에서 구문 분석기는 증가 후 처리를 수행하기위한 코드를 생성하기 전에 추가 연산을 수행하기위한 코드를 생성하므로, 증가해야합니다. 임시 사본을 작성하십시오. 여기서 성능 킬러는 증가하지 않습니다. 오버로드 된 infix 연산자를 사용합니다. 삽입 연산자는 새 인스턴스를 생성합니다.
bit-twiddler

2
사람들이 '사용되는'이유 가이 질문 / 답변에서 언급 된 특정 인기 프로그래밍 언어의 이름 때문일 i++++i입니다.
Shadow

61

항상 프로그래머를 먼저 코딩하고 컴퓨터를 둘째로 코딩하십시오.

컴파일러는 코드를 통해 자사의 전문 시선을 던졌다, 후 성능 차이가 존재하는 경우, 그리고 당신이 그것을 측정 할 수 있습니다 다음 변경할 수 있습니다 - 중요하다.


7
SUPERB 선언문 !!!
Dave

8
@ 마틴 : 이것이 바로 접두사 증가를 사용하는 이유입니다. Postfix 의미는 이전 값을 유지한다는 것을 의미하며, 필요하지 않으면 사용하기가 정확하지 않습니다.
Matthieu M.

1
더 명확한 루프 인덱스의 경우-포인터를 늘려서 배열을 반복하고 접두사를 사용하는 경우 시작 전에 잘못된 주소에서 시작하여 성능 향상에 관계없이 좋지 않은 것으로 시작하는 것을 의미하는 경우
Martin Beckett

5
@Matthew : 사후 증가가 이전 값의 사본을 유지한다는 것을 의미하지는 않습니다. 컴파일러가 출력을 볼 때까지 중간 값을 처리하는 방법을 확신 할 수 없습니다. 주석이 달린 GCC 생성 어셈블리 언어 목록을 보는 데 시간이 걸리면 GCC가 두 루프에 대해 동일한 머신 코드를 생성한다는 것을 알 수 있습니다. 더 효율적이기 때문에 사후 증분보다 사전 증분을 선호하는 것에 대한이 넌센스는 추측보다 더 적습니다.
bit-twiddler

2
@Mathhieu : 내가 게시 한 코드는 최적화가 해제 된 상태에서 생성되었습니다. C ++ 사양에서는 사후 증분이 사용될 때 컴파일러가 값의 임시 인스턴스를 생성해야한다고 명시하지 않습니다. 사전 증분 및 사후 증분 연산자의 우선 순위 만 나타냅니다.
bit-twiddler

13

GCC는 두 루프에 대해 동일한 머신 코드를 생성합니다.

C 코드

int main(int argc, char** argv)
{
    for (int i = 0; i < 42; i++)
            printf("i = %d\n",i);

    for (int i = 0; i < 42; ++i)
        printf("i = %d\n",i);

    return 0;
}

조립 코드 (내 의견과 함께)

    cstring
LC0:
    .ascii "i = %d\12\0"
    .text
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L9
"L00000000001$pb":
L9:
    popl    %ebx
    movl    $0, -16(%ebp)  // -16(%ebp) is "i" for the first loop 
    jmp L2
L3:
    movl    -16(%ebp), %eax   // move i for the first loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub    // call printf
    leal    -16(%ebp), %eax  // make the eax register point to i
    incl    (%eax)           // increment i
L2:
    cmpl    $41, -16(%ebp)  // compare i to the number 41
    jle L3              // jump to L3 if less than or equal to 41
    movl    $0, -12(%ebp)   // -12(%ebp) is "i" for the second loop  
    jmp L5
L6:
    movl    -12(%ebp), %eax   // move i for the second loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub     // call printf
    leal    -12(%ebp), %eax  // make eax point to i
    incl    (%eax)           // increment i
L5:
    cmpl    $41, -12(%ebp)   // compare i to 41 
    jle L6               // jump to L6 if less than or equal to 41
    movl    $0, %eax
    addl    $36, %esp
    popl    %ebx
    leave
    ret
    .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
L_printf$stub:
    .indirect_symbol _printf
    hlt ; hlt ; hlt ; hlt ; hlt
    .subsections_via_symbols

최적화가 설정된 상태는 어떻습니까?
serv-inc

2
@user : 아마도 아무 변화도 없지만 실제로 비트 트위저가 곧 다시 올 것으로 기대하십니까?
중복 제거기

2
주의 : C에는 오버로드 연산자가있는 사용자 정의 유형이 없지만 C ++에는 기본 유형에서 사용자 정의 유형으로 일반화하는 것이 단순히 유효하지 않습니다 .
중복 제거기

@ 중복 제거기 :이 답변이 사용자 정의 유형으로 일반화되지 않는다는 점도 지적합니다. 묻기 전에 그의 사용자 페이지를 보지 않았습니다.
serv-inc

12

97 %의 시간으로 성능에 대해 걱정하지 마십시오. 조기 최적화는 모든 악의 근원입니다.

-도널드 크 누스

이제 이것이 우리의 길을 벗어 났으므로 , 우리의 선택을 산만 하게 만들어 보자 .

  • ++i: 접두사 증가 , 현재 값 증가 및 결과 산출
  • i++: 접미사 증가 , 값 복사, 현재 값 증가 , 사본 생성

이전 값의 사본이 필요하지 않은 경우 접미사 증가 를 사용하면 작업을 수행 할 수 있습니다.

부정확성 은 게으름에서 비롯되며 항상 가장 직접적인 방식으로 자신의 의도를 표현하는 구조를 사용하십시오. 미래의 관리자가 원래 의도를 오해 할 가능성이 적습니다.

비록 (사실상) 사소하지만, 코드를 읽음으로써 정말로 당황한 때가 있습니다. 의도와 실제 표현이 일치했는지 궁금합니다. 물론 몇 개월 후에 그들은 (또는 I) 기억 나지 않았다 ...

따라서 그것이 당신에게 맞는지 아닌지는 중요하지 않습니다. 포옹 키스 . 몇 달 안에 예전 연습을 피할 수 있습니다.


4

C ++에서, 당신은 할 수 관련 연산자 오버로드가있는 경우에 당신이있는 거 작성 템플릿 코드와 반복자가 전달 될 수 모르겠어요 특히, 상당한 성능 차이를 확인하십시오. 모든 반복자 X 뒤에 논리는 모두 실질적이고 significant- 될 수있다 즉, 컴파일러가 느리고 최적화 할 수 없습니다.

그러나 C의 경우에는 그다지 사소한 유형 일 뿐이며 성능 차이는 사소한 것이며 컴파일러는 쉽게 최적화 할 수 있습니다.

팁 : C 또는 C ++로 프로그래밍하면 질문이 둘 다가 아니라 둘 중 하나와 관련됩니다.


2

두 작업의 성능은 기본 아키텍처에 따라 크게 달라집니다. 메모리에 저장된 값을 증가시켜야하는데, 이는 폰 노이만 병목 현상이 두 경우 모두 제한 요인이라는 것을 의미합니다.

++ i의 경우에는

Fetch i from memory 
Increment i
Store i back to memory
Use i

i ++의 경우에는

Fetch i from memory
Use i
Increment i
Store i back to memory

++ 및-연산자는 원점을 PDP-11 명령어 세트로 추적합니다. PDP-11은 레지스터에서 자동 사후 증가를 수행 할 수 있습니다. 또한 레지스터에 포함 된 유효 주소에서 자동 사전 감소를 수행 할 수도 있습니다. 두 경우 모두 해당 변수가 "register"변수 인 경우 컴파일러는 이러한 시스템 수준 작업 만 활용할 수 있습니다.


2

느린 것이 있는지 알고 싶다면 테스트하십시오. BigInteger 또는 이와 동등한 것을 가져 와서 두 관용구를 사용하여 유사한 for 루프에 붙이고 루프 내부가 최적화되지 않았는지 확인하고 둘 다 시간을 정하십시오.

기사를 읽은 후 세 가지 이유로 매우 설득력이 없습니다. 하나는, 컴파일러는 결코 사용되지 않은 객체의 생성을 최적화 할 수 있어야합니다. 둘째,이 i++개념은 숫자 for 루프대해 관용적 이므로 실제로 영향을받는 사례는로 제한됩니다. 셋째, 그들은 순전히 이론적 인 주장을 제시하며,이를 뒷받침 할 숫자는 없습니다.

이유 # 1을 기반으로, 내 생각에 당신이 실제로 타이밍을 할 때, 그들은 서로 바로 옆에있을 것입니다.


-1

우선 가독성 IMO에 영향을 미치지 않습니다. 그것은 당신이 이전에 보았던 것이 아니지만 그것에 익숙해지기까지는 단지 짧은 시간입니다.

두 번째로 코드에 많은 접미사 연산자를 사용하지 않으면 큰 차이가 없을 것입니다. 가능할 때 사용하지 않는 주된 논점은 원래의 var 값을 여전히 사용할 수있는 인수가 끝날 때까지 원본 var 값의 사본을 보관해야한다는 것입니다. 아키텍처에 따라 32 비트 또는 64 비트입니다. 이는 4 또는 8 바이트 또는 0.00390625 또는 0.0078125MB와 같습니다. 오늘날의 컴퓨터 리소스와 속도로 매우 긴 기간 동안 저장해야하는 많은 톤을 사용하지 않으면 접두사에서 접두사로 전환 할 때의 차이를 느끼지 못할 가능성이 매우 높습니다.

편집 : 내 결론이 거짓으로 판명 되었으므로이 나머지 부분을 잊어 버리십시오 (++ i 및 i ++의 일부는 항상 똑같은 일을하지는 않습니다 ... 여전히 사실입니다).

또한 사례에서 동일한 작업을 수행하지 않는 것으로 이전에 지적되었습니다. 결정하면 스위치를 만들 때주의하십시오. 나는 그것을 시도하지 않았습니다 (항상 postfix를 사용했습니다). 잘 모르겠지만 postfix에서 prefix로 변경하면 다른 결과가 발생할 것이라고 생각합니다 (다시 잘못 될 수 있습니다 ... 컴파일러에 따라 다릅니다. / 통역사도)

for (int i=0; i < 10; i++) //the set of i values here will be {0,1,2,3,4,5,6,7,8,9}
for (int i=0; i < 10; ++i) //the set of i values here will be {1,2,3,4,5,6,7,8,9,10}

4
증가 연산은 for 루프의 끝에서 발생하므로 정확히 동일한 출력을 갖습니다. 컴파일러 / 인터프리터에 의존하지 않습니다.
jsternberg

@jsternberg ... 감사합니다. 실제로 테스트 할 이유가 없었기 때문에 증가가 언제 발생했는지 확신하지 못했습니다. 대학에서 컴파일러를 사용한 지 너무 오래되었습니다! lol
케네스

잘못 틀렸다
ruohola

-1

나는 의미, 생각 ++i보다 더 의미가 i++내가 처음에 충실 것, 그래서 를 제외하고 그것의 일반적인 (사용합니다 자바처럼 그렇게하지에 i++이 널리 사용되고 있기 때문에).


-2

성능에만 관한 것이 아닙니다.

때로는 의미가 없기 때문에 복사를 전혀 사용하지 않으려는 경우가 있습니다. 그리고 접두사 증가의 사용은 이것에 의존하지 않기 때문에 접두사 형식을 고수하는 것이 훨씬 간단합니다.

프리미티브 유형과 복합 유형에 대해 다른 증분을 사용하면 실제로 읽을 수 없습니다.


-2

정말로 필요하지 않으면 ++ i를 고수 할 것입니다. 대부분의 경우, 이것이 의도 한 것입니다. i ++가 필요한 경우는 그리 많지 않으며, 그러한 구조를 읽을 때 항상 두 번 생각해야합니다. ++ i를 사용하면 쉽습니다. 1을 더하고 사용하면 여전히 동일합니다.

따라서 @martin beckett에 전적으로 동의합니다. 더 쉽게 만들 수 있습니다. 이미 어렵습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.