C ++에서 변수를 캐시해야합니까, 아니면 컴파일러가 최적화를 수행하도록해야합니까? (앨리어싱)


114

다음 코드를 고려하십시오 ( p유형 unsigned char*이며 bitmap->width정수 유형이며 정확히 알 수 없으며 사용중인 일부 외부 라이브러리의 버전에 따라 다릅니다).

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

최적화 할 가치가 있습니까 [..]

다음과 같이 작성하여보다 효율적인 결과를 얻을 수있는 경우가있을 수 있습니다.

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

... 또는 컴파일러가 최적화하는 것이 사소한가요?

"더 나은"코드라고 생각하는 것은 무엇입니까?

편집자 주석 (Ike) : 삼진 텍스트에 대해 궁금해하는 사람들에게 원래 질문은 표현 된대로 주제를 벗어난 영역에 위험 할 정도로 가까웠으며 긍정적 인 피드백에도 불구하고 종결에 매우 가까웠습니다. 이것들은 다쳤습니다. 그러나 질문의 ​​이러한 문제를 해결 한 답변자들을 처벌하지 마십시오.


19
If *pis the same type as widththen 그것은 최적화하는 것이 쉬운 일이 아닙니다 . 루프 내에서 그것을 p가리키고 width수정할 수 있기 때문입니다.
emlai 2011

31
컴파일러가 특정 작업을 최적화하는지 여부를 묻는 것은 일반적으로 잘못된 질문입니다. (일반적으로) 궁극적으로 관심있는 것은 어떤 버전이 더 빠르게 실행되는지, 간단히 측정해야하는 것입니다.
SirGuy 2015

4
@GuyGreer 나는 그 질문이 좋거나 적어도 흥미 롭다고 말하고 싶지만, 안타깝게도 대답은 "사용 사례에 따라 측정해야합니다"라고 생각했습니다. 그 이유는 기능은 이식 가능하지만 성능은 그렇지 않기 때문입니다. 따라서 실제로 컴파일러에서 시작하여 대상 사이트 (OS / 하드웨어 조합)에서 완료하는 빌드 프로세스의 모든 부분에 따라 다릅니다. 물론 가장 좋은 추측은 컴파일러가 인간보다 똑똑하다는 것입니다.
luk32

19
내가 컴파일러라면 두 예제가 동일하지 않다는 것을 알 수 있습니다. .NET p과 동일한 메모리 를 가리킬 수 bitmap->width있습니다. 따라서 첫 번째 예제를 두 번째 예제로 합법적으로 최적화 할 수 없습니다.
Mysticial 2011

4
"p"는 어디에 저장됩니까? "char * restrict p2 = p;"와 같은 작업을 수행하면 성능이 매우 크게 향상 될 수 있습니다. 그런 다음 루프 내에서 "p"대신 "p2"를 사용합니다. 그런 다음 "p2"에 대한 변경 사항을 p에 다시 적용하려면 "p + = (p2-p);"를 사용합니다. p2에서 복사되지 않은 포인터에 의해 p2의 수명 내에 쓰여진 포인터는 p2에서 복사 된 포인터를 사용하여 읽을 수 없으며 그 반대도 마찬가지이며, p2의 수명 이후에는 어떤 목적으로도 p2의 복사본을 사용할 수 없지만 컴파일러는이를 사용할 수 있습니다. 다른 수단으로는 달성 할 수없는 최적화를 가능하게하는 사실.
supercat

답변:


81

언뜻보기에는 컴파일러가 최적화 플래그가 활성화 된 두 버전 모두에 대해 동등한 어셈블리를 생성 할 수 있다고 생각했습니다. 확인했을 때 결과를보고 놀랐습니다.

출처 unoptimized.cpp

참고 :이 코드는 실행되지 않습니다.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    for (unsigned x = 0 ; x < static_cast<unsigned>(bitmap.width) ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

출처 optimized.cpp

참고 :이 코드는 실행되지 않습니다.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    const unsigned width = static_cast<unsigned>(bitmap.width);
    for (unsigned x = 0 ; x < width ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

편집

  • $ g++ -s -O3 unoptimized.cpp
  • $ g++ -s -O3 optimized.cpp

어셈블리 (최적화되지 않음)

    .file   "unoptimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    mov %eax, %edx
    addl    $1, %eax
    movq    (%rsi,%rdx,8), %rdx
    movb    $0, (%rdx)
    cmpl    bitmap(%rip), %eax
    jb  .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

조립 (최적화 됨)

    .file   "optimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    subl    $1, %eax
    leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    movq    (%rsi,%rax), %rdx
    addq    $8, %rax
    cmpq    %rcx, %rax
    movb    $0, (%rdx)
    jne .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

차이

$ diff -uN unoptimized.s optimized.s
--- unoptimized.s   2015-11-24 16:11:55.837922223 +0000
+++ optimized.s 2015-11-24 16:12:02.628922941 +0000
@@ -1,4 +1,4 @@
-   .file   "unoptimized.cpp"
+   .file   "optimized.cpp"
    .text
    .p2align 4,,15
 .globl main
@@ -10,16 +10,17 @@
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
+   subl    $1, %eax
+   leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
 .L3:
-   mov %eax, %edx
-   addl    $1, %eax
-   movq    (%rsi,%rdx,8), %rdx
+   movq    (%rsi,%rax), %rdx
+   addq    $8, %rax
+   cmpq    %rcx, %rax
    movb    $0, (%rdx)
-   cmpl    bitmap(%rip), %eax
-   jb  .L3
+   jne .L3
 .L2:
    xorl    %eax, %eax
    ret

실제로 (부하 않는 최적화 된 버전을 생성 조립체 lea)을 width계산 최적화되지 않은 버전과 달리 일정하게 width반복 될 때마다 오프셋 ( movq)을.

시간이 생기면 결국 벤치 마크를 게시합니다. 좋은 질문.


3
최적화되지 않은 경우 const unsigned대신 캐스트하면 코드가 다르게 생성되었는지 확인하는 것이 흥미로울 것 unsigned입니다.
Mark Ransom 2015

2
@MarkRansom 나는 그것이 차이를해서는 안 추측 : CONST 인의 "약속"을하지 전체 루프 만 단일 비교시입니다
하겐 폰 Eitzen

13
제발 절대로 기능을 사용하지 main최적화를위한 테스트합니다. Gcc는 의도적으로 콜드로 표시하므로 일부 최적화를 비활성화합니다. 그게 여기에 해당되는지는 모르겠지만, 들어가야 할 중요한 습관입니다.
Marc Glisse 2015

3
@MarcGlisse 당신이 100 % 맞아요. 서둘러 작성 했으니 개선하겠습니다.
YSC 2015

3
여기에 대한 링크입니다 godbolt에 하나 개의 컴파일 단위에서 두 기능은 가정, bitmap글로벌입니다. 비 CSEd 버전은 cmp이 경우 perf에 문제가되지 않는에 메모리 피연산자를 사용합니다 . 만약 그것이 로컬이라면 컴파일러는 다른 포인터가 그것에 대해 "알지 못"하고 그것을 가리킬 수 없다고 가정 할 수 있습니다. 가독성이 향상되거나 성능이 중요하다면 임시 변수에 전역을 포함하는 표현식을 저장하는 것은 나쁜 생각이 아닙니다. 많은 일이 일어나지 않는 한 그러한 지역 주민들은 일반적으로 레지스터에 살 수 있으며 절대 유출되지 않습니다.
Peter Cordes 2015

38

실제로 코드 스 니펫에서 알 수있는 정보가 충분하지 않습니다. 제가 생각할 수있는 한 가지는 앨리어싱입니다. 우리의 관점에서, 그것은 매우 당신이 원하는하지 않는 것이 분명 p하고 bitmap메모리에 같은 위치를 가리 키도록하지만 (때문에 컴파일러는 그것을 알고하지 않는 p타입이다 char*컴파일러는 경우에도이 코드가 작동을한다) p그리고 bitmap겹칩니다.

이것은이 경우 루프 bitmap->width가 포인터 p를 통해 변경 되면 bitmap->width나중에 다시 읽을 때 볼 수 있어야 하며 이는 다시 로컬 변수에 저장하는 것이 불법임을 의미합니다.

즉, 일부 컴파일러는 실제로 때때로 동일한 코드의 두 가지 버전을 생성 할 것이라고 믿습니다 (이에 대한 상황 적 증거를 보았지만이 경우 컴파일러가 수행하는 작업에 대한 정보를 직접 찾지는 않았습니다). 별칭을 사용하고 괜찮다고 판단되면 더 빠른 코드를 실행합니다.

즉, 두 버전의 성능을 단순히 측정하는 것에 대한 내 의견을지지합니다. 내 돈은 두 버전의 코드간에 일관된 성능 차이를 보지 않는 데 있습니다.

제 생각에, 컴파일러 최적화 이론과 기술에 대해 배우는 것이 목적이라면 이와 같은 질문은 괜찮지 만, 여기서 최종 목표가 프로그램을 더 빠르게 실행하는 것이라면 시간 낭비입니다 (쓸모없는 마이크로 최적화).


1
@GuyGreer : 주요 최적화 차단기입니다. 나는 언어 규칙이 다른 항목의 쓰기와 읽기가 순서가 없거나 순서가없는 상황을 식별하기보다는 효과적인 유형에 대한 규칙에 초점을 맞추는 것이 불행하다고 생각합니다. 이러한 용어로 작성된 규칙은 현재의 것보다 컴파일러와 프로그래머의 요구를 훨씬 더 잘 충족시킬 수 있습니다.
supercat 2015

3
@GuyGreer- restrict이 경우 한정자가 앨리어싱 문제에 대한 답이 아닐까요?
LThode 2015

4
내 경험상 restrict대부분 뺑소니입니다. MSVC는 내가 본 유일한 컴파일러로 제대로 작동하는 것 같습니다. ICC는 인라인 된 경우에도 함수 호출을 통해 별칭 정보를 잃습니다. 그리고 GCC는 모든 단일 입력 매개 변수를 restrict( this멤버 함수 포함) 으로 선언하지 않는 한 일반적으로 이점을 얻지 못합니다 .
Mysticial

1
@Mystical : 기억해야 할 한 가지는 char모든 유형의 별칭을 지정 한다는 것이므로 char *가 있으면 restrict모든 것에 사용해야 합니다. 또는 GCC의 엄격한 앨리어싱 규칙을 해제 한 -fno-strict-aliasing경우 모든 것이 가능한 앨리어스로 간주됩니다.
Zan Lynx

1
@Ray restrictC ++의 유사 의미론에 대한 가장 최근의 제안 은 N4150 입니다.
TC

24

좋아, 얘들 아, GCC -O3(Linux x64에서 GCC 4.9 사용)으로 측정했습니다 .

두 번째 버전은 54 % 더 빠르게 실행됩니다!

그래서 나는 앨리어싱이 문제라고 생각합니다. 나는 그것에 대해 생각하지 않았습니다.

[편집하다]

모든 포인터가로 정의 된 첫 번째 버전을 다시 시도 __restrict__했으며 결과는 동일합니다. 이상한 .. 앨리어싱이 문제가 아니거나 어떤 이유로 컴파일러가 __restrict__.

[편집 2]

좋습니다. 앨리어싱이 문제라는 것을 거의 증명할 수 있었던 것 같습니다. 이번에는 포인터가 아닌 배열을 사용하여 원래 테스트를 반복했습니다.

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

그리고 측정되었습니다 (연결하려면 "-mcmodel = large"를 사용해야 함). 그런 다음 시도했습니다.

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

측정 결과는 동일했습니다. 컴파일러가 자체적으로 최적화 할 수 있었던 것 같습니다.

그런 다음 원래 코드 (포인터 사용 p)를 시도했는데 이번에 p는 유형이 std::uint16_t*. 다시 말하지만, 엄격한 앨리어싱으로 인해 결과는 동일했습니다. 그런 다음 "-fno-strict-aliasing"을 사용하여 빌드를 시도했고 다시 시간 차이를 확인했습니다.


4
이것은 기술적으로 질문에 대한 답변을 제공하지만 주석이어야하는 것처럼 보입니다. 또한 불행히도 앨리어싱이 문제임을 입증하지 않았습니다. 확실히 그럴듯 해 보이지만 그게 다라고 결론을 내리는 것과는 다릅니다.
SirGuy

@GuyGreer : 내 [편집 2]를 참조하십시오-이제 거의 입증 된 것 같습니다.
Yaron Cohen-Tal

2
루프에 "x"가있을 때 변수 "i"를 사용하기 시작한 이유가 궁금합니다.
Jesper Madsen 2015

1
54 % 더 빨리 이해하기 어려운 구절을 찾는 것은 나뿐 입니까? 최적화되지 않은 것보다 1.54 배 빠른 속도라는 뜻입니까?
Roddy 2015

3
@ YaronCohen-Tal이 두 배 이상 빠르나요? 인상적이지만 "54 % 더 빠르다"는 의미는 아닙니다.
Roddy 2015

24

다른 답변은 루프에서 포인터 작업을 끌어 올리는 것이 char이 무엇이든 별칭을 지정할 수 있도록 허용하는 별칭 규칙으로 인해 정의 된 동작을 변경할 수 있으므로 대부분의 경우 분명히 인간에게 정확하더라도 컴파일러에 대해 허용 가능한 최적화가 아니라는 점을 지적했습니다. 프로그램 제작자.

그들은 또한 작업을 루프 밖으로 끌어 올리는 것이 일반적으로 성능 측면에서 개선되는 것은 아니지만 가독성 측면에서 종종 부정적이라고 지적했습니다.

나는 종종 "세 번째 방법"이 있다는 것을 지적하고 싶습니다. 원하는 반복 횟수를 계산하는 대신 0까지 계산할 수 있습니다. 즉, 반복 횟수는 루프 시작시 한 번만 필요하며 이후에 저장할 필요가 없습니다. 어셈블러 수준에서 더 나은 경우에는 감소 작업이 일반적으로 카운터가 감소 이전 (캐리 플래그)과 이후 (제로 플래그) 모두 0인지 여부를 나타내는 플래그를 설정하므로 명시적인 비교가 필요하지 않는 경우가 많습니다.

for (unsigned x = static_cast<unsigned>(bitmap->width);x > 0;  x--)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

이 버전의 루프는 0 .. (width-1) 범위가 아닌 1..width 범위의 x 값을 제공합니다. 실제로 x를 사용하는 것이 아니기 때문에 귀하의 경우에는 중요하지 않지만 알아야 할 사항입니다. 0 .. (width-1) 범위의 x 값으로 카운트 다운 루프를 원한다면 할 수 있습니다.

for (unsigned x = static_cast<unsigned>(bitmap->width); x-- > 0;)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

비트 맵-> 너비로 수행하는 모든 작업은 변수에 직접 할당하기 때문에 비교 규칙에 미치는 영향에 대해 걱정하지 않고 원하는 경우 위의 예에서 캐스트를 제거 할 수도 있습니다.


2
두 번째 사례는으로 형식이 지정되어 x --> 0"downto"연산자가 생성되는 것을 보았습니다 . 꽤 재밌어. 추신 : 가독성을 위해 최종 조건에 대한 변수를 음수로 만드는 것을 고려하지 않습니다. 실제로는 그 반대 일 수 있습니다.
Mark Ransom

정말 상황에 따라 다르지만, 때로는 문장이 너무 끔찍해서 여러 문장으로 나누면 가독성이 향상되지만 여기에 해당한다고 믿지 않습니다.
plugwash

1
+1 좋은 관찰이지만 , 루프에서 대신 static_cast<unsigned>(bitmap->width)사용 width하는 것이 실제로 가독성을 향상시키는 것이라고 주장하고 싶습니다. 이제 독자가 한 줄에 구문 분석 할 것이 적기 때문입니다. 그러나 다른 사람들의 견해는 다를 수 있습니다.
SirGuy 2015

1
아래로 세는 것이 더 좋은 다른 많은 상황이 있습니다 (예 : 목록에서 항목을 제거 할 때). 왜 이것이 더 자주 이루어지지 않는지 모르겠습니다.
이안 Goldby

3
최적의 asm처럼 보이는 루프를 작성하려면를 사용하십시오 do { } while(). ASM에서는 끝에 조건부 분기가있는 루프를 만들기 때문입니다. 컴파일러가 항상 적어도 한 번 실행된다는 것을 증명할 수없는 경우 일반 for(){}while(){}루프는 루프 전에 루프 조건을 한 번 테스트하기위한 추가 명령이 필요합니다. 반드시 루프가 한 번도 실행되어야하는지 또는 더 읽기 쉬운 지 확인하는 것이 유용 할 때 for()또는 사용 while()하십시오.
Peter Cordes 2015

11

여기에서 최적화를 막을 수있는 유일한 것은 엄격한 앨리어싱 규칙 입니다. 한마디로 :

"엄격한 앨리어싱은 C (또는 C ++) 컴파일러에 의해 만들어진 가정으로, 다른 유형의 객체에 대한 포인터를 역 참조하는 것은 동일한 메모리 위치를 참조하지 않을 것입니다 (즉, 서로 앨리어싱)."

[…]

규칙에 대한 예외는 char*모든 유형을 가리킬 수있는입니다.

예외는 unsignedsigned char포인터 에도 적용됩니다 .

이 코드에서의 경우 : 당신 수정 *p을 통해 p되는 unsigned char*컴파일러 있도록 해야한다 그것을 가리 수 있다고 가정합니다 bitmap->width. 따라서의 캐싱은 bitmap->width잘못된 최적화입니다. 이 최적화 방지 동작은 YSC의 답변에 나와 있습니다.

유형이 아닌 유형을 p가리키는 경우에만 캐싱이 가능한 최적화가 될 수 있습니다.chardecltype(bitmap->width)


10

원래 질문은 다음과 같습니다.

최적화 할 가치가 있습니까?

그리고 그것에 대한 나의 대답 (상승 표와 반대표의 좋은 조합을 모으는 것 ..)

컴파일러가 그것에 대해 걱정하게하십시오.

컴파일러는 거의 확실히 당신보다 더 나은 일을 할 것입니다. 그리고 '최적화'가 '명백한'코드보다 낫다는 보장은 없습니다. 측정 했습니까 ??

더 중요한 것은 최적화하는 코드가 프로그램 성능에 영향을 미친다는 증거가 있습니까?

반대표에도 불구하고 (이제 앨리어싱 문제가 있음) 여전히 유효한 답변으로 만족합니다. 무언가를 최적화 할 가치가 있는지 모른다면 아마도 그렇지 않을 것입니다.

물론 다소 다른 질문은 다음과 같습니다.

코드 조각을 최적화 할 가치가 있는지 어떻게 알 수 있습니까?

첫째, 애플리케이션 또는 라이브러리를 현재보다 빠르게 실행해야합니까? 사용자가 너무 오래 기다 립니까? 소프트웨어가 내일이 아닌 어제의 날씨를 예측합니까?

당신의 소프트웨어가 무엇을위한 것인지 그리고 당신의 사용자가 기대하는 바를 바탕으로 당신 만이 이것을 말할 수 있습니다.

소프트웨어에 최적화가 필요하다고 가정하면 다음으로 할 일은 측정을 시작하는 것입니다. 프로파일 러는 코드가 시간을 보내는 위치를 알려줍니다. 조각이 병목 현상으로 표시되지 않는 경우에는 그대로 두는 것이 가장 좋습니다. 프로파일 러 및 기타 측정 도구는 변경 사항이 영향을 미쳤는지 알려줍니다. 코드를 최적화하는 데 몇 시간을 할애 할 수 있지만 눈에 띄는 차이가 없다는 사실 만 알 수 있습니다.

어쨌든 '최적화'란 무엇을 의미합니까?

'최적화 된'코드를 작성하지 않는 경우 코드는 가능한 한 명확하고 간결하며 간결해야합니다. "조기 최적화는 사악하다"라는 주장은 엉성하거나 비효율적 인 코드에 대한 변명이 아닙니다.

최적화 된 코드는 일반적으로 성능을 위해 위의 일부 속성을 희생합니다. 추가 지역 변수를 도입하거나 예상 범위보다 넓은 객체를 갖거나 일반 루프 순서를 반대로하는 것이 포함될 수 있습니다. 이 모든 것들은 덜 명확하거나 간결 할 수 있으므로이 작업을 수행하는 이유에 대한 코드를 (간단하게!) 문서화하십시오.

그러나 종종 '느린'코드에서는 이러한 마이크로 최적화가 최후의 수단입니다. 가장 먼저 살펴볼 곳은 알고리즘과 데이터 구조입니다. 일을 아예 피하는 방법이 있습니까? 선형 검색을 이진 검색으로 바꿀 수 있습니까? 여기에서 연결된 목록이 벡터보다 빠를까요? 아니면 해시 테이블? 결과를 캐시 할 수 있습니까? 여기에서 좋은 '효율적인'결정을 내리는 것은 종종 성능에 수십 배 이상 영향을 미칠 수 있습니다!


12
비트 맵 이미지의 너비에 대해 반복 할 때 루프 논리는 루프에 소요되는 시간의 상당 부분이 될 수 있습니다. 이 경우에는 조기 최적화에 대해 걱정하기보다는 처음부터 효율적인 모범 사례를 개발하는 것이 좋습니다.
Mark Ransom 2015

4
@MarkRansom은 부분적으로 동의했습니다. 그러나 "모범 사례"는 a : 이미지 채우기를 위해 기존 라이브러리 또는 API 호출을 사용하거나 b : GPU가 자동으로 수행하도록합니다. OP가 제안하는 측정되지 않은 마이크로 최적화가되어서는 안됩니다. 그리고이 코드가 한 번 이상 실행되거나 16 픽셀 너비보다 큰 비트 맵으로 실행되는지 어떻게 알 수 있습니까?
Roddy 2015

@Veedrac. -1에 대한 정당성을 인정하십시오. 내가 대답 한 이후 질문의 추진력은 미묘하고 실질적으로 변경되었습니다. (확장 된) 대답이 여전히 도움이되지 않는다고 생각한다면, 제가 그것을 삭제할 시간입니다. "그게 가치가 있나 ..."는 항상 주로 의견 기반입니다.
Roddy

@Roddy 나는 편집에 감사하고, 그들은 도움을줍니다 (그리고 내 의견은 어쨌든 너무 가혹하게 들렸습니다). 이것은 실제로 Stack Overflow에 적합하지 않은 질문에 대한 답변이기 때문에 여전히 울타리에 있습니다. 여기에 높은 투표를받은 답변이 그렇듯이 적절한 답변이 스 니펫에 특정한 것처럼 느껴집니다.
Veedrac 2015

6

이와 같은 상황에서 다음 패턴을 사용합니다. 그것은 당신의 첫 번째 경우만큼 짧고 두 번째 경우보다 낫습니다. 임시 변수를 루프에 로컬로 유지하기 때문입니다.

for (unsigned int x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
  *p++ = 0xAA;
  *p++ = 0xBB;
  *p++ = 0xCC;
}

이는 스마트 컴파일러, 디버그 빌드 또는 특정 컴파일 플래그보다 더 빠릅니다.

Edit1 : 루프 외부에 일정한 작업을 배치하는 것은 좋은 프로그래밍 패턴입니다. 특히 C / C ++에서 기계 작동의 기초에 대한 이해를 보여줍니다. 나는 자신을 증명하려는 노력이이 관행을 따르지 않는 사람들에게되어야한다고 주장합니다. 컴파일러가 좋은 패턴에 대해 처벌한다면 컴파일러의 버그입니다.

Edit2 : : vs2013의 원본 코드에 대한 내 제안을 측정하여 % 1 개선되었습니다. 더 잘할 수 있습니까? 간단한 수동 최적화는 이국적인 명령에 의존하지 않고 x64 시스템의 원래 루프보다 3 배 개선됩니다. 아래 코드는 리틀 엔디안 시스템과 적절하게 정렬 된 비트 맵을 가정합니다. TEST 0은 원본 (9 초)이고 TEST 1은 더 빠릅니다 (3 초). 누군가가 이것을 더 빨리 만들 수 있고 테스트 결과는 비트 맵의 ​​크기에 따라 달라집니다. 조만간 컴파일러는 일관되게 가장 빠른 코드를 생성 할 수있을 것입니다. 나는 이것이 컴파일러가 프로그래머 AI가 될 미래가 될 것이므로 우리는 일을 할 수 없을 것입니다. 그러나 지금은 루프에서 추가 작업이 필요하지 않다는 것을 알 수있는 코드를 작성하십시오.

#include <memory>
#include <time.h>

struct Bitmap_line
{
  int blah;
  unsigned int width;
  Bitmap_line(unsigned int w)
  {
    blah = 0;
    width = w;
  }
};

#define TEST 0 //define 1 for faster test

int main(int argc, char* argv[])
{
  unsigned int size = (4 * 1024 * 1024) / 3 * 3; //makes it divisible by 3
  unsigned char* pointer = (unsigned char*)malloc(size);
  memset(pointer, 0, size);
  std::unique_ptr<Bitmap_line> bitmap(new Bitmap_line(size / 3));
  clock_t told = clock();
#if TEST == 0
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
    //for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#else
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    unsigned x = 0;
    for (const unsigned n = static_cast<unsigned>(bitmap->width) - 4; x < n; x += 4)
    {
      *(int64_t*)p = 0xBBAACCBBAACCBBAALL;
      p += 8;
      *(int32_t*)p = 0xCCBBAACC;
      p += 4;
    }

    for (const unsigned n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#endif
  double ms = 1000.0 * double(clock() - told) / CLOCKS_PER_SEC;
  printf("time %0.3f\n", ms);

  {
    //verify
    unsigned char* p = pointer;
    for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      if ((*p++ != 0xAA) || (*p++ != 0xBB) || (*p++ != 0xCC))
      {
        printf("EEEEEEEEEEEEERRRRORRRR!!!\n");
        abort();
      }
    }
  }

  return 0;
}

int64_t 및 int32_t 대신 int64_t 3 개를 사용하면 64 비트에서 25 %를 더 절약 할 수 있습니다.
Antonín Lejsek 2015

5

고려해야 할 두 가지 사항이 있습니다.

A) 최적화는 얼마나 자주 실행됩니까?

사용자가 버튼을 클릭 할 때처럼 대답이 그리 자주 나오지 않는 경우 코드를 읽을 수 없게 되어도 신경 쓰지 마십시오. 답이 1 초에 1000 번이라면 아마도 최적화를 원할 것입니다. 조금 복잡하더라도 다음 사람을 돕기 위해 무슨 일이 일어나고 있는지 설명하기 위해 의견을 달아야합니다.

B) 이로 인해 코드 유지 / 문제 해결이 더 어려워 집니까?

성능이 크게 향상되지 않는 경우 단순히 몇 개의 클럭 틱을 저장하기 위해 코드를 암호화하는 것은 좋은 생각이 아닙니다. 많은 사람들이 좋은 프로그래머라면 코드를보고 무슨 일이 일어나고 있는지 알아낼 수 있어야한다고 말할 것입니다. 사실입니다. 문제는 비즈니스 세계에서 그것을 알아내는 데 추가 시간이 소요된다는 것입니다. 따라서 읽기를 더 예쁘게 만들 수 있다면 그렇게하십시오. 당신의 친구들이 그것에 대해 감사 할 것입니다.

그것은 개인적으로 B 예제를 사용하겠다고 말했습니다.


4

컴파일러는 많은 것을 최적화 할 수 있습니다. 귀하의 예를 들어, 가독성, 조작성 및 코드 표준을 따르는 것을 고려해야합니다. 최적화 할 수있는 항목 (GCC 사용)에 대한 자세한 내용은 이 블로그 게시물을 참조하십시오 .


4

일반적으로 사용자가 인계해야한다고 결정할 때까지 컴파일러가 최적화를 수행하도록합니다. 이것에 대한 논리는 성능과 관련이 없으며 오히려 인간의 가독성과 관련이 있습니다. 에서 광대 의 경우 대부분, 프로그램의 가독성은 성능보다 더 중요하다. 사람이 읽기 쉬운 코드를 작성하는 것을 목표로해야하며, 코드의 유지 보수 가능성보다 성능이 더 중요하다고 확신 할 때만 최적화에 대해 걱정해야합니다.

성능이 중요하다는 것을 알게되면 코드에서 프로파일 러를 실행하여 어떤 루프가 비효율적인지 확인하고 개별적으로 최적화해야합니다. 실제로 이러한 최적화를 수행하려는 경우가있을 수 있지만 (특히 STL 컨테이너가 관련된 C ++로 마이그레이션하는 경우) 가독성 측면에서 비용이 많이 듭니다.

또한 실제로 코드 속도를 늦출 수있는 병리학 적 상황을 생각할 수 있습니다. 예를 들어 컴파일러가 bitmap->width프로세스를 통해 일정 함을 증명할 수없는 경우를 생각해보십시오 . width변수를 추가하면 컴파일러가 해당 범위에서 로컬 변수를 유지하도록 강제합니다. 특정 플랫폼 특정 이유로 인해 추가 변수가 스택 공간 최적화를 방해한다면 바이트 코드를 내보내는 방법을 재구성하고 효율성이 떨어지는 것을 생성해야 할 수 있습니다.

예를 들어, Windows x64 __chkstk에서 함수가 두 페이지 이상의 지역 변수를 사용하는 경우 함수의 서문에서 특수 API 호출을 호출해야 합니다. 이 기능은 필요할 때 스택을 확장하는 데 사용하는 가드 페이지를 창에 관리 할 수있는 기회를 제공합니다. 추가 변수가 스택 사용량을 1 페이지 아래에서 1 페이지 이상으로 올리면 이제 함수 __chkstk가 입력 될 때마다 호출해야합니다 . 느린 경로에서이 루프를 최적화하려면 실제로 느린 경로에서 저장 한 것보다 빠른 경로를 더 느리게 할 수 있습니다!

물론 약간 병리 적이지만이 예제의 요점은 실제로 컴파일러 속도를 늦출 수 있다는 것입니다. 최적화가 진행되는 위치를 결정하기 위해 작업을 프로파일 링해야 함을 보여줍니다. 그동안 중요 할 수도 있고 중요하지 않을 수도있는 최적화를 위해 가독성을 희생하지 마십시오.


4
나는 C와 C ++가 프로그래머가 신경 쓰지 않는 것을 명시 적으로 식별하는 더 많은 방법을 제공하기를 바랍니다. 컴파일러가 최적화 할 수있는 더 많은 기회를 제공 할뿐만 아니라 코드를 읽는 다른 프로그래머가 예를 들어 매번 비트 맵-> 폭을 다시 확인하여 루프에 영향을 주는지 확인해야 하는지를 추측하지 않아도됩니다. 변경 사항이 루프에 영향을 미치지 않도록 비트 맵-> 너비를 캐싱할지 여부. "캐시 여부-상관 없어"라고 말하는 수단이 있으면 프로그래머가 선택한 이유가 명확 해집니다.
supercat 2015

@supercat 나는이 문제를 해결하기 위해 쓰려고했던 문신이있는 실패한 언어 더미를 보면 알 수 있기 때문에 전심으로 동의합니다. 나는 경건하지 않은 구문없이 누군가가 신경 쓰지 않는 "무엇"을 정의하기가 매우 어렵다는 것을 발견했습니다. 나는 헛된 수색을 계속한다.
Cort Ammon 2015

모든 경우에 정의 할 수는 없지만 유형 시스템이 도움이 될 수있는 경우가 많이 있다고 생각합니다. C는 모든 유형에 적용될 수있는 "휘발성"보다 약간 느슨한 유형 한정자를 갖는 대신 문자 유형을 "범용 접근 자"로 만들기로 결정했습니다. 이러한 유형의 액세스는 다음과 같은 순서로 처리 될 것입니다. 규정되지 않은 등가 유형의 액세스와 동일한 규정자를 가진 모든 유형의 변수에 대한 액세스. 문자 유형을 사용했는지 여부를 명확히하는 데 도움이 될 것입니다 ...
supercat

... 별칭 행동, 또는 자신의 필요에 맞는 크기이기 때문에 사용했는지 여부. 문자 유형 액세스와 관련된 암시 적 장벽과 달리 많은 경우 루프 외부에 배치 될 수있는 명시 적 앨리어싱 장벽을 갖는 것도 도움이 될 것입니다.
supercat 2015

1
이것은 현명한 이야기이지만 일반적으로 이미 작업에 C를 선택했다면 성능이 매우 중요하며 다른 규칙이 적용되어야합니다. 그렇지 않으면 Ruby, Java, Python 등을 사용하는 것이 더 나을 수 있습니다.
Audrius Meskauskas 2015

4

비교는 잘못 두 코드 조각 이후

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x<width ;  ++x)

동등하지 않다

첫 번째 경우 width는 종속적이고 const가 아니므로 후속 반복 사이에 변경되지 않을 수 있다고 가정 할 수 없습니다. 따라서 최적화 할 수는 없지만 모든 루프에서 확인해야합니다 .

최적화 된 경우 프로그램 실행 중 특정 시점에 지역 변수 에의 값이 할당됩니다 bitmap->width. 컴파일러는 이것이 실제로 변경되지 않는지 확인할 수 있습니다.

다중 스레딩에 대해 생각 했습니까, 아니면 값이 휘발성이되도록 값이 외부 적으로 의존적 일 수 있습니다. 당신이 말하지 않으면 컴파일러가이 모든 것을 알아 내기를 어떻게 기대할까요?

컴파일러는 코드가 허용하는만큼만 좋은 일을 할 수 있습니다.


2

컴파일러가 코드를 어떻게 최적화하는지 정확히 알지 못한다면 코드 가독성과 디자인을 유지하여 자체 최적화를 수행하는 것이 좋습니다. 실제로 새로운 컴파일러 버전을 위해 작성하는 모든 함수에 대한 어셈블리 코드를 확인하는 것은 어렵습니다.


1

bitmap->width값이 width반복 사이에 변경 될 수 있으므로 컴파일러는 최적화 할 수 없습니다 . 가장 일반적인 몇 가지 이유가 있습니다.

  1. 멀티 스레딩. 컴파일러는 다른 스레드가 값을 변경하려고하는지 예측할 수 없습니다.
  2. 루프 내부에서 수정, 때로는 변수가 루프 내부에서 변경되는지 여부를 알려주는 것이 간단하지 않습니다.
  3. 이 함수 호출 예이다 iterator::end()또는 container::size()그래서 항상 같은 결과를 반환 할 경우 예측하기 어렵다.

높은 수준의 최적화가 필요한 곳을 요약하면 (내 개인적인 의견) 혼자서 할 필요가 있습니다. 다른 곳에서는 그냥두면 컴파일러가 최적화할지 여부를 결정할 수 있습니다. 코드 가독성이 주요 목표입니다.

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