2016-12-24 7 views
1

std :: array < uint64_t, ...>에서 데이터를 읽을 때 컴파일러가 초기 벡터로드를 사용하지 못하게하는 문제가 있습니다.정렬 된 std :: array에서 초기 자동 벡터화로드가 스칼라 인 이유는 무엇입니까? (g ++/clang ++)

gcc가 -fopt-info-vec- *를 사용하여 디버그 정보를 생성 할 수 있다는 것을 알고 있습니다. 왜 두 컴파일러가 초기 스칼라로드를 사용하는 것과 같은 차선의 결정을 내리는 지 알 수있는 자세한 로그에서 찾을 수 없습니다.

반면에 나는 벡터화 문제에 대한 자세한 정보를 제공하기 위해 clang을 만드는 법을 모릅니다. -Rpass-analysis = init에서 루프가 인터리빙 할 가치가 없다는보고 만 루프 벡터 라이 제이션합니다. 물론 내 고유 버전은 루프가 벡터화 될 수 있지만 필수 변환은 컴파일러를 제외하고는 너무 복잡하다는 증거입니다.

물론 내장 함수를 사용하여 핫 경로를 구현할 수는 있지만 각 CPU 논리에 대해 동일한 논리를 복제해야합니다. 나는 컴파일러가 완벽하게 벡터 라이 제이션 할 수있는 stanard C++ 코드를 작성하는 것을 선호한다. target_clones 속성이나 매크로 및 대상 속성을 사용하여 여러 플래그로 여러 번 동일한 코드를 간단하게 컴파일 할 수 있습니다.

컴파일러가로드가 벡터화되지 않은 이유를 말해주는 방법은 무엇입니까?

gcc가 이미 해당 정보를 인쇄하고있는 것으로 의심됩니다. 내가 뭘 찾고 있는지 모릅니다.

초기로드시 자동 벡터화가 실패하는 이유는 무엇입니까?

/** 
    * This is a test case removing abstraction layers from my actual code. My 
    * real code includes one extra problem that access to pack loses alignment 
    * information wasn't only issue. Compilers still generate 
    * suboptimal machine code with alignment information present. I fail to 
    * understand why loads are treated differently compared to stores to 
    * same address when auto-vectorization is used. 
    * 
    * I tested gcc 6.2 and clang 3.9 
    * g++ O3 -g -march=native vectest.cc -o vectest -fvect-cost-model=unlimited 
    * clang++ -O3 -g -march=native vectest.cc -o vectest 
    */ 


    #include <array> 
    #include <cstdint> 

    alignas(32) std::array<uint64_t, 52> pack; 
    alignas(32) uint64_t board[4]; 

    __attribute__((noinline)) 
    static void init(uint64_t initial) 
    { 
     /* Clang seem to prefer large constant table and unrolled copy 
     * which should perform worse outside micro benchmark. L1 misses 
     * and memory bandwidth are bigger bottleneck than alu instruction 
     * execution. But of course this code won't be compiled to hot path so 
     * I don't care how it is compiled as long as it works correctly. 
     * 
     * But most interesting detail from clang is vectorized stores are 
     * generated correctly like: 
    4005db:  vpsllvq %ymm2,%ymm1,%ymm2 
    4005e0:  vmovdqa %ymm2,0x200a78(%rip)  # 601060 <pack> 
    4005e8:  vpaddq 0x390(%rip),%ymm0,%ymm2  # 400980 <_IO_stdin_used+0x60> 
    4005f0:  vpsllvq %ymm2,%ymm1,%ymm2 
    4005f5:  vmovdqa %ymm2,0x200a83(%rip)  # 601080 <pack+0x20> 
    4005fd:  vpaddq 0x39b(%rip),%ymm0,%ymm2  # 4009a0 <_IO_stdin_used+0x80> 
     * 
     * gcc prefers scalar loop. 
     */ 

     for (unsigned i = 0; i < pack.size(); i++) { 
      pack[i] = 1UL << (i + initial); 
     } 
    } 

    #include "immintrin.h" 
    __attribute__((noinline)) 
    static void expected_init(uint64_t initial) 
    { 
     /** Just an intrinsic implementation of init that would be IMO ideal 
     * optimization. 
     */ 
    #if __AVX2__ 
     unsigned i; 
     union { 
      uint64_t *mem; 
      __m256i *avx; 
     } conv; 
     conv.mem = &pack[0]; 
     __m256i t = _mm256_set_epi64x(
       1UL << 3, 
       1UL << 2, 
       1UL << 1, 
       1UL << 0 
       ); 
     /* initial is just extra random number to prevent constant array 
     * initialization 
     */ 
     t = _mm256_slli_epi64(t, initial); 
     for(i = 0; i < pack.size()/4; i++) { 
      _mm256_store_si256(&conv.avx[i], t); 
      t = _mm256_slli_epi64(t, 4); 
     } 
    #endif 
    } 

    __attribute__((noinline)) 
    static void iter_or() 
    { 
     /** initial load (clang): 
    4006f0:  vmovaps 0x200988(%rip),%xmm0  # 601080 <pack+0x20> 
    4006f8:  vorps 0x200960(%rip),%xmm0,%xmm0  # 601060 <pack> 
    400700:  vmovaps 0x200988(%rip),%xmm1  # 601090 <pack+0x30> 
    400708:  vorps 0x200960(%rip),%xmm1,%xmm1  # 601070 <pack+0x10> 
    400710:  vinsertf128 $0x1,%xmm1,%ymm0,%ymm0 
     * expected: 
    400810:  vmovaps 0x200868(%rip),%ymm0  # 601080 <pack+0x20> 
    400818:  vorps 0x200840(%rip),%ymm0,%ymm0  # 601060 <pack> 
    400820:  vorps 0x200878(%rip),%ymm0,%ymm0  # 6010a0 <pack+0x40> 
     */ 

     auto iter = pack.begin(); 
     uint64_t n(*iter++), 
      e(*iter++), 
      s(*iter++), 
      w(*iter++); 
     for (;iter != pack.end();) { 
      n |= *iter++; 
      e |= *iter++; 
      s |= *iter++; 
      w |= *iter++; 
     } 
     /** Store is correctly vectorized to single instruction */ 
     board[0] = n; 
     board[1] = e; 
     board[2] = s; 
     board[3] = w; 
    } 

    __attribute__((noinline)) 
    static void index_or() 
    { 
     /** Clang compiles this to same as iterator variant. gcc goes 
     * completely insane. I don't even want to try to guess what all the 
     * permutation stuff is trying to archive. 
     */ 
     unsigned i; 
     uint64_t n(pack[0]), 
      e(pack[1]), 
      s(pack[2]), 
      w(pack[3]); 
     for (i = 4 ; i < pack.size(); i+=4) { 
      n |= pack[i+0]; 
      e |= pack[i+1]; 
      s |= pack[i+2]; 
      w |= pack[i+3]; 
     } 
     board[0] = n; 
     board[1] = e; 
     board[2] = s; 
     board[3] = w; 
    } 

    #include "immintrin.h" 

    __attribute__((noinline)) 
    static void expected_result() 
    { 
     /** Intrinsics implementation what I would expect auto-vectorization 
     * transform my c++ code. I simple can't understand why both compilers 
     * fails to archive results I expect. 
     */ 
    #if __AVX2__ 
     union { 
      uint64_t *mem; 
      __m256i *avx; 
     } conv; 
     conv.mem = &pack[0]; 
     unsigned i; 
     __m256i res = _mm256_load_si256(&conv.avx[0]); 
     for (i = 1; i < pack.size()/4; i++) { 
      __m256i temp = _mm256_load_si256(&conv.avx[i]); 
      res = _mm256_or_si256(res, temp); 
     } 
     conv.mem = board; 
     _mm256_store_si256(conv.avx, res); 
    #endif 
    } 

    int main(int c, char **v) 
    { 
     (void)v; 
     expected_init(c - 1); 
     init(c - 1); 

     iter_or(); 
     index_or(); 
     expected_result(); 
    } 

답변

0

gcc와 clang 모두 외부 루프의 초기로드를 벡터화하지 못하는 것으로 보입니다. 임시 변수를 0으로 변경 한 다음 첫 번째 요소에서 또는을 사용하면 컴파일러가 더 잘 수행됩니다. Clang은 좋은 unrolled vector code를 생성합니다 (하나의 ymm 레지스터 만 병목 현상을 일으키고 모든 명령은 이전 명령에 대한 종속성을 갖습니다). GCC는 여분의 초기 vpxor 및 반복 당 하나의 vpor을 수행하는 꽤 나쁜 루프를 사용하여 조금 더 나쁜 코드를 생성합니다.

또한 마이크로 벤치 마크 베스트가 교대 레지스터로 개선 된 확장되지 않은 코드가되는 몇 가지 대안 구현을 테스트했습니다.

/* only reduce (calling this function from a for loop): 
* ST 7.3 cycles (ST=single thread) 
* SMT 15.3 cycles (SMT=simultaneous multi threading aka hyper threading) 
* shuffle+reduce (calling Fisher-Yatas shuffle and then this function): 
* ST 222 cycles 
* SMT 383 cycles 
*/ 
    "vmovaps 0x00(%0), %%ymm0\n" 
    "vmovaps 0x20(%0), %%ymm1\n" 
    "vpor 0x40(%0), %%ymm0, %%ymm0\n" 
    "vpor 0x60(%0), %%ymm1, %%ymm1\n" 
    "vpor 0x80(%0), %%ymm0, %%ymm0\n" 
    "vpor 0xA0(%0), %%ymm1, %%ymm1\n" 
    "vpor 0xC0(%0), %%ymm0, %%ymm0\n" 
    "vpor 0xE0(%0), %%ymm1, %%ymm1\n" 
    "vpor 0x100(%0), %%ymm0, %%ymm0\n" 
    "vpor 0x120(%0), %%ymm1, %%ymm1\n" 
    "vpor 0x140(%0), %%ymm0, %%ymm0\n" 
    "vpor 0x160(%0), %%ymm1, %%ymm1\n" 
    "vpor 0x180(%0), %%ymm0, %%ymm0\n" 

    "vpor %%ymm0, %%ymm1, %%ymm0\n" 
    "vmovaps %%ymm0, 0x00(%1)\n" 

연타는 루프

/* only reduce: 
* ST 9.8 cycles 
* SMT 21.8 cycles 
* shuffle+reduce: 
* ST 223 cycles 
* SMT 385 cycles 
*/ 

그러나 SMT가 풀린 코드의 성능을 감소 숫자가 의심스러운 모습 같은 타이밍이 풀려. 나는 아직 풀린 것보다 분명히 느린 GCC 루프를 더 잘 작성하려고 노력했다. 하지만 두 개의 레지스터를 사용하고 루프를 한 번 풀면 명령어 의존성을 깨기로 결정했습니다. 그 결과 완전히 언 롤링하는 것보다 약간 더 빠르게 셔플 + 코드를 줄일 수있었습니다.

size_t end = pack.size() - 3*4; 
asm (
/* The best SMT option outside micro optimization. 
* This allows executing two vpor instructions same time and 
* reduces loop count to half with single unroll 
* 
* only reduce: 
* ST 13.0 cycles 
* SMT 20.0 cycles 
* shuffle+reduce: 
* ST 221 cycles 
* SMT 380 cycles 
*/ 
    "vmovaps 0x180(%[pack]), %%ymm0\n" 
    "vmovaps 0x160(%[pack]), %%ymm1\n" 
    "vpor 0x00(%[pack],%[cnt],8), %%ymm0, %%ymm0\n" 
    "1:\n" 
    "vpor -0x20(%[pack],%[cnt],8), %%ymm1, %%ymm1\n" 
    "vpor -0x40(%[pack],%[cnt],8), %%ymm0, %%ymm0\n" 
    "sub $8, %[cnt]\n" 
    "jne 1b\n" 

    "vpor %%ymm0, %%ymm1, %%ymm0\n" 
    "vmovaps %%ymm0, 0x00(%[out])\n" 
    : [cnt]"+r"(end) 
    : [pack]"r"(begin), [out]"r"(hands_)); 

그러나 피셔 - 예이츠 셔플 후 코드를 실행하면 차이가 놀랍습니다. 명확한 gcc 버전은 줄이기 벤치 마크 (16.4/38.8)에서 동일한 속도 (228/387)에 가까운 셔플 + 테스트 감소 실행을 잃게됩니다.