development

C ++에서 변수를 캐시하거나 컴파일러가 최적화하도록해야합니까?

big-blog 2020. 7. 24. 07:15
반응형

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


다음 코드를 고려하십시오 ( 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)의 메모 : 파업 텍스트에 대해 궁금한 사람들은 원래 질문이 주 제외 영역에 가깝고 긍정적 인 피드백에도 불구하고 닫히는 데 매우 가깝습니다. 이것들은 쫓겨났다. 그러나이 문제의 문제를 해결 한 답변자들을 처벌하지 마십시오.


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

출처 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)합니다 ( ).widthwidthmovq

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


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

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

즉, 일부 컴파일러는 실제로 동일한 코드의 두 가지 버전을 생성 할 수 있다고 생각합니다 (정확한 증거를 보았지만이 경우 컴파일러가 수행중인 작업에 대한 정보를 직접 찾지 못했습니다). 포인터가 있는지 빨리 확인하십시오. 별명으로 판단하고 더 빠른 코드를 실행하면 괜찮습니다.

즉, 나는 두 버전의 성능을 단순히 측정하는 것에 대한 내 의견으로, 두 가지 버전의 코드간에 일관된 성능 차이가 보이지 않습니다.

내 의견으로는, 컴파일러 최적화 이론과 기술에 대해 배우는 것이 목적이라면 이러한 질문은 괜찮지 만, 최종 목표가 프로그램을 더 빨리 실행하는 것이 시간 낭비라면 쓸모없는 마이크로 최적화입니다.


다른 답변에 따르면 루프에서 포인터 작업을 올리면 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;
}

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


좋아, 여러분, 나는 GCC -O3(Linux x64에서 GCC 4.9를 사용하여) 측정했습니다 .

두 번째 버전은 54 % 더 빠릅니다.

그래서, 나는 앨리어싱 (aliasing)이라고 생각합니다.

[편집하다]

로 정의 된 모든 포인터로 첫 번째 버전을 다시 시도 __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"을 사용하여 빌드를 시도한 후 다시 시간의 차이를 확인했습니다.


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

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

[…]

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

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

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

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


질문은 원래 물었다 :

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

그리고 그것에 대한 나의 대답은 (위와 아래로 투표를 잘 혼합 한 것입니다.)

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

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

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

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

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

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

먼저 응용 프로그램이나 라이브러리를 현재보다 빠르게 실행해야합니까? 사용자가 너무 오래 기다리나요? 소프트웨어가 내일 날씨가 아닌 어제 날씨를 예측합니까?

소프트웨어의 용도와 사용자가 기대하는 바에 따라 사용자 만이 사실을 알 수 있습니다.

소프트웨어에 최적화가 필요하다고 가정하면 다음으로해야 할 일은 측정을 시작하는 것입니다. 프로파일 러는 코드 사용 시간을 알려줍니다. 조각이 병목 현상으로 표시되지 않으면 혼자 두는 것이 가장 좋습니다. 프로파일 러 및 기타 측정 도구도 변경 사항이 변경되었는지 알려줍니다. 코드를 최적화하기 위해 몇 시간을 소비 할 수 있으며, 눈에 띄는 차이는 없습니다.

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

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

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

그러나 종종 '느린'코드를 사용하면 이러한 미세 최적화가 최후의 수단입니다. 우선 살펴볼 것은 알고리즘과 데이터 구조입니다. 일을 전혀 피하는 방법이 있습니까? 선형 검색을 이진 검색으로 바꿀 수 있습니까? 벡터보다 연결된 목록이 더 빠를까요? 아니면 해시 테이블? 결과를 캐시 할 수 있습니까? 여기에서 '효율적인'올바른 결정을 내리는 것은 종종 성능에 영향을 줄 수 있습니다.


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

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;
}

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

A) How often will the optimization run?

If the answer is not very often, like only when a user clicks a button, then don't bother if it makes your code unreadable. If the answer is 1000 times a second then you will probably want to go with the optimization. If it is even a bit complex be sure to put a comment in to explain what is going on to help the next guy that comes along.

B) Will this make the code harder to upkeep/troubleshoot?

If you're not seeing a huge gain in performance then making your code cryptic simply to save a few clock ticks is not a good idea. Lots of people will tell you that any good programmer should be able to look at the code and figure out what is going on. This is true. The problem is that in the business world the extra time figuring that out costs money. So, if you can make it prettier to read then do it. Your friends will thank you for it.

That said I'd personally use the B example.


The compiler is able to optimize a lot of things. For your example, you should go for the readability, mantainability and what follows your code standard. For more information about what can be optimized (with GCC), see this blog post.


As a general rule, let the compiler do the optimization for you, until you determine that you should take over. The logic for this has nothing to do with performance, but rather with human readability. In the vast majority of cases, the readability of your program is more important than its performance. You should aim to write code which is easier for a human to read, and then only worry about optimization when you are convinced that performance is more important than the maintainability of your code.

Once you do see that performance matters, you should run a profiler on the code to determine which loops are being inefficient, and optimize those individually. There may indeed be cases where you want to do that optimization (especially if you migrate towards C++, where STL containers get involved), but the cost in terms of readability is great.

In addition, I can think of pathological situations where it could actually slow the code down. For example, consider the case where the compiler could not prove that bitmap->width was constant through the process. By adding the width variable you force the compiler to maintain a local variable in that scope. If, for some platform specific reason, that extra variable prevented some stack-space optimization, it may have to reorganize how it is emitting bytecodes, and produce something less efficient.

As an example, on Windows x64, one is obliged to call a special API call, __chkstk in the preamble of the function if the function will use more than 1 page of local variables. This function gives windows a chance to manage the guard pages they use to expand the stack when needed. If your extra variable pushes the stack usage up from below 1 page to at-or-above 1 page, your function is now obliged to call __chkstk every time it is entered. If you were to optimize this loop on a slow path, you may actually slow the fast path down more than you saved on the slow path!

Sure, it's a bit pathological, but the point of that example is that you can actually slow the compiler down. It just shows that you do have to profile your work to determine where the optimizations go. In the mean time, please don't sacrifice readability in any way for an optimization that may or may not matter.


The comparison is wrong since the two code snippets

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

and

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

are not equivalent

In the first case width is dependent and not const, and one cannot assume that it may not change between subsequent iterations. Thus it cannot be optimized, but has to be checked at every loop.

In your optimized case a local variable is assigned the value of bitmap->width at some point during program execution. The compiler can verify that this does not actually change.

Did you think about multi threading , or maybe the value could be externally dependent such that its value is volatile. How would one expect the compiler to figure all these things out if you do not tell?

The compiler can only do as good as your code lets it.


Unless you know how exactly the compiler optimizes the code, it is better to do your own optimizations by keeping the code readability, and design. Practically it is hard to check assembly code for every function we write for new compiler versions.


Compiler cannot optimize bitmap->width because value of width can be changed between iterations. There are a few most common reasons:

  1. Multi-threading. Compiler cannot predict if other thread is about to change value.
  2. Modification inside loop, sometimes it is not simple to tell if variable will be changed inside loop.
  3. It is function call, e.g. iterator::end() or container::size() so it is hard to predict if it will always returns the same result.

To sum up (my personal opinion) for places that requires high level of optimization you need to do that by yourself, in other places just leave it, compiler may optimize it or not, if there is no big difference code readability is main target.

참고URL : https://stackoverflow.com/questions/33898371/in-c-should-i-bother-to-cache-variables-or-let-the-compiler-do-the-optimizat

반응형