2016-12-01 6 views
9

C++ 표현식 템플릿을 사용하여 값 배열에서 작동하는 SSE2 및 AVX 코드를 간단하게 작성하는 매우 간단한 프로그램을 테스트하고 있습니다.간단한 C++ 표현식 템플릿 내장 내장 함수는 다른 명령어를 생성합니다.

값 배열을 나타내는 svec 클래스가 있습니다.

SSE2 이중 레지스터를 나타내는 sreg 클래스가 있습니다.

expradd_expr은어레이의 추가를 나타냅니다.

컴파일러는 수식 코드 테스트 케이스와 비교하여 루프 당 3 개의 추가 명령어를 생성합니다. 이것에 대한 이유가 있는지 또는 동일한 출력을 생성하기 위해 컴파일러를 얻을 수있는 변경 사항이 있는지 궁금합니다.

전체 테스트 환경은 다음과 같습니다

00007FF621CD1BC0 mov   rdx,qword ptr [c] 
00007FF621CD1BC5 mov   rcx,qword ptr [rcx] 
00007FF621CD1BC8 mov   rax,qword ptr [r8] 
00007FF621CD1BCB vmovupd  xmm0,xmmword ptr [r9+rax] 
00007FF621CD1BD1 vaddpd  xmm1,xmm0,xmmword ptr [rcx+r9] 
00007FF621CD1BD7 vaddpd  xmm0,xmm1,xmmword ptr [rdx+r9] 
00007FF621CD1BDD lea   rax,[r9+rbx] 
00007FF621CD1BE1 vaddpd  xmm0,xmm0,xmmword ptr [rax+r10] 
00007FF621CD1BE7 vmovupd  xmmword ptr [rax],xmm0 
00007FF621CD1BEB add   r9,10h 
00007FF621CD1BEF cmp   r9,400h 
00007FF621CD1BF6 jae   main+154h (07FF621CD1C04h) # extra instruction 1 
00007FF621CD1BF8 mov   rcx,qword ptr [rsp+60h]  # extra instruction 2 
00007FF621CD1BFD mov   r8,qword ptr [rsp+58h]  # extra instruction 3 
00007FF621CD1C02 jmp   main+110h (07FF621CD1BC0h) 

이가 참고 : 표현

00007FF621CD1B70 mov   r8,qword ptr [c] 
00007FF621CD1B75 mov   rdx,qword ptr [b] 
00007FF621CD1B7A mov   rax,qword ptr [a] 
00007FF621CD1B7F vmovupd  xmm0,xmmword ptr [rcx+rax] 
00007FF621CD1B84 vaddpd  xmm1,xmm0,xmmword ptr [rdx+rcx] 
00007FF621CD1B89 vaddpd  xmm3,xmm1,xmmword ptr [r8+rcx] 
00007FF621CD1B8F lea   rax,[rcx+rbx] 
00007FF621CD1B93 vaddpd  xmm1,xmm3,xmmword ptr [r10+rax] 
00007FF621CD1B99 vmovupd  xmmword ptr [rax],xmm1 
00007FF621CD1B9D add   rcx,10h 
00007FF621CD1BA1 cmp   rcx,400h 
00007FF621CD1BA8 jb   main+0C0h (07FF621CD1B70h) 

버전이 템플릿 : 손을 들어

#include <iostream> 
#include <emmintrin.h> 

struct sreg 
{ 
    __m128d reg_; 

    sreg() {} 

    sreg(const __m128d& r) : 
     reg_(r) 
    { 
    } 

    sreg operator+(const sreg& b) const 
    { 
     return _mm_add_pd(reg_, b.reg_); 
    } 
}; 

template <typename T> 
struct expr 
{ 
    sreg operator[](std::size_t i) const 
    { 
     return static_cast<const T&>(*this).operator[](i); 
    } 

    operator const T&() const 
    { 
     return static_cast<const T&>(*this); 
    } 
}; 

template <typename A, typename B> 
struct add_expr : public expr<add_expr<A, B>> 
{ 
    const A& a_; 
    const B& b_; 

    add_expr(const A& a, const B& b) : 
     a_{ a }, b_{ b } 
    { 
    } 

    sreg operator[](std::size_t i) const 
    { 
     return a_[i] + b_[i]; 
    } 
}; 

template <typename A, typename B> 
inline auto operator+(const expr<A>& a, const expr<B>& b) 
{ 
    return add_expr<A, B>(a, b); 
} 

struct svec : public expr<svec> 
{ 
    sreg* regs_; 
    std::size_t size_; 

    svec(std::size_t size) : 
     size_{ size } 
    { 
     regs_ = static_cast<sreg*>(_aligned_malloc(size * 32, 32)); 
    } 

    ~svec() 
    { 
     _aligned_free(regs_); 
    } 

    template <typename T> 
    svec& operator=(const T& expression) 
    { 
     for (std::size_t i = 0; i < size(); i++) 
     { 
      regs_[i] = expression[i]; 
     } 

     return *this; 
    } 

    const sreg& operator[](std::size_t index) const 
    { 
     return regs_[index]; 
    } 

    sreg& operator[](std::size_t index) 
    { 
     return regs_[index]; 
    } 

    std::size_t size() const 
    { 
     return size_; 
    } 
}; 

static constexpr std::size_t size = 64; 

int main() 
{ 
    svec a(size); 
    svec b(size); 
    svec c(size); 
    svec d(size); 
    svec vec(size); 

    //hand rolled loop 
    for (std::size_t j = 0; j < size; j++) 
    { 
     vec[j] = a[j] + b[j] + c[j] + d[j]; 
    } 

    //expression templates version of hand rolled loop 
    vec = a + b + c + d; 

    std::cout << "Done..."; 

    std::getchar(); 

    return EXIT_SUCCESS; 
} 

이 지침은 루프를 압연 특별히 입증 할 수있는 최소한의 검증 가능한 코드. 이 코드는 2015 업데이트를 Visual Studio에서 내가 할인 한 3

아이디어를 기본 릴리스 빌드 설정을 사용하여 컴파일 :

  • 손이 루프 롤 루프의 순서 (이미 전환하고 표현 컴파일러는 여전히 추가 지침을 삽입하고 않는 경우 루프 템플릿 컴파일러는 손 size가 정전류 회로는 것을 추론 컴파일러를 방지 constexprsize (나는 이미 시도 테스트 코드를 기반으로 루프를 압연 최적화되어)

  • 을 확인 핸드 롤 루프를 더 잘 최적화하기 위해서는 nt가 필요하며 핸드 롤 루프의 지침에는 아무런 차이가 없습니다).

+0

첫 번째 추가 명령은 실제로 추가 명령이 아닙니다. 다른 사람들은 아마도 "완료"를 인쇄했을 것입니다. – Drop

+0

@Drop, 나쁜 추측은 아닙니다. 루프의 순서를 바꾸는 코드를 컴파일하고 루프의 순서 템플릿 버전으로 루프의 순서와 관련된 컴파일러 문제를 피하기 위해 루프를 굴려서 불행하게도 아무런 차이가 없습니다. – keith

+2

부수적으로 : 디스 어셈블리를 의미있게 만들기 위해서는 입력 데이터를 동적으로 제공하는 것이 좋습니다. 즉, 컴파일 타임에 입력을 알 수 없어야합니다. 스마트 컴파일러는 결과를 상수 표현으로 평가할 수 있다면 코드를 완전히 또는 부분적으로 최적화 할 수 있습니다 – Drop

답변

3

두 루프가 반복 될 때마다 배열 포인터를 다시로드하는 것처럼 보입니다. (예 : 첫 번째 루프에 mov r8, [c]). 두 번째 버전은 간접적 인 두 가지 수준으로 더욱 비효율적으로 수행하고 있습니다. 그 중 하나가 루프의 끝에서 나오고, 조건 분기 후에 루프에서 빠져 나옵니다.

"신품"으로 식별되지 않은 변경된 지침 중 하나는 mov rcx, [rcx]입니다. 레지스터 할당은 루프간에 다르지만 배열 시작 포인터입니다. 상점 (상점 뒤의 rcx,[rsp+60h])은 mov rax,qword ptr [a]을 대체합니다. 난 a 또한 RSP에서 오프셋이며 실제로 정적 스토리지에 대한 레이블이 아니라고 가정합니다. MSVC++는 vec[j]에 저장 포인터을 수정할 수 없음을 증명하기 위해 별칭 분석에 성공하지 않았기 때문에


은 아마도이 일어나고있다.필자는 템플릿을 신중하게 보지 않았지만 최적화를 기대하는 추가 수준의 간접 참조를 도입하는 경우 문제는 그것이 아니라는 것입니다.

분명한 해결책은 최적화가 더 좋은 컴파일러를 사용하는 것입니다. clang3.9는 잘 돌아가며 (포인터의 재로드없이 자동 벡터 라이 제이션) gcc는 결과가 사용되지 않기 때문에 완전히 최적화합니다.

MSVC가 붙어있는 경우, 별다른 별칭이없는 옵션이나 별칭이없는 키워드 또는 선언이 있는지 확인하면 도움이됩니다. 예 : GNU C++ 확장에는 __restrict__이 포함되어있어 "this does not alias"동작을 C99의 restrict 키워드로 사용합니다. MSVC가 그런 것을 지원한다면 IDK.


NIT-선택 :

그것은 jae에 "추가"명령을 호출하는 것은 매우 옳지 않아. 그것은 단지 jb의 정반대의 술어이므로 이제는 do{...}while() 루프 대신에 while(true){ ... if() break; reload; } 루프가됩니다. (나는 C 구문을 사용하여 asm 루프 구조를 보여줍니다. 실제로 C 루프를 실제로 컴파일하면 컴파일러에서 최적화 할 수 있습니다.) 따라서 "추가 명령어"는 무조건 분기 인 JMP입니다.

+1

이것은 자리 잡았습니다. 코드를 수정하여 앨리어싱 문제를 없애고 완벽하게 최적화되었습니다. 나는이 단계를 독자적으로 할 수 없었을 것이다 :-) – keith

+0

@keith : 실용적인 개선을 이끌어 냈다는 것을 듣고 기쁘다. :) –

2

이 문제를 겪는 다른 모든 사람들에게, MSVC가 내가 본 문제점없이 최적화 할 수있는 앨리어스가없는 버전이 있습니다. 나는 약간의 메타 프로그래밍을 사용하여 과부하가 너무 욕심 거리는 것을 막아야했다. 간단한 방법이 경우 "표현이"어떻게 작동하는지에 대한 정보의 비트를 요구 한 권리 단서 ... @ 피터 코르에

#include <iostream> 
#include <utility> 
#include <type_traits> 
#include <emmintrin.h> 

class sreg 
{ 
    using reg_type = __m128d; 

public: 
    reg_type reg_; 

    sreg() {} 

    sreg(const reg_type& r) : 
     reg_(r) 
    { 
    } 

    sreg operator+(const sreg& b) const 
    { 
     return _mm_add_pd(reg_, b.reg_); 
    } 
}; 

struct expr 
{ 
}; 

template <typename... Ts> 
struct meta_or : std::false_type 
{ 
}; 

template <typename T, typename... Ts> 
struct meta_or<T, Ts...> : std::integral_constant<bool, T::value || meta_or<Ts...>::value> 
{ 
}; 

template <class... T> 
using meta_is_expr = meta_or<std::is_base_of<expr, std::decay_t<T>>..., std::is_base_of<expr, T>...>; 

template <class... T> 
using meta_enable_if_expr = std::enable_if_t<meta_is_expr<T...>::value>; 

template <typename A, typename B> 
struct add_expr : public expr 
{ 
    A a_; 
    B b_; 

    add_expr(A&& a, B&& b) : 
     a_{ std::forward<A>(a) }, b_{ std::forward<B>(b) } 
    { 
    } 

    sreg operator[](std::size_t i) const 
    { 
     return a_[i] + b_[i]; 
    } 
}; 

template <typename A, typename B, typename = meta_enable_if_expr<A, B>> 
inline auto operator+(A&& a, B&& b) 
{ 
    return add_expr<A, B>{ std::forward<A>(a), std::forward<B>(b) }; 
} 

struct svec : public expr 
{ 
    sreg* regs_;; 
    std::size_t size_; 

    svec(std::size_t size) : 
     size_{ size } 
    { 
     regs_ = static_cast<sreg*>(_aligned_malloc(size * 32, 32)); 
    } 

    ~svec() 
    { 
     _aligned_free(regs_); 
    } 

    template <typename T> 
    svec& operator=(const T& expression) 
    { 
     for (std::size_t i = 0; i < size(); i++) 
     { 
      regs_[i] = expression[i]; 
     } 

     return *this; 
    } 

    const sreg& operator[](std::size_t index) const 
    { 
     return regs_[index]; 
    } 

    sreg& operator[](std::size_t index) 
    { 
     return regs_[index]; 
    } 

    std::size_t size() const 
    { 
     return size_; 
    } 
}; 

static constexpr std::size_t size = 64; 

int main() 
{ 
    svec a(size); 
    svec b(size); 
    svec c(size); 
    svec d(size); 
    svec vec(size); 

    //hand rolled loop 
    for (std::size_t j = 0; j < size; j++) 
    { 
     vec[j] = a[j] + b[j] + c[j] + d[j]; 
    } 

    //expression templates version of hand rolled loop 
    vec = a + b + c + d; 

    std::cout << "Done..."; 

    std::getchar(); 

    return EXIT_SUCCESS; 
} 

많은 감사 원더. 우리 svec 단일 루프를 들어

는 할당 연산자에 발생합니다

template <typename T> 
svec& operator=(const T& expression) 
{ 
    for (std::size_t i = 0; i < size(); i++) 
    { 
     regs_[i] = expression[i]; 
    } 

    return *this; 
} 

운영자 과부하 :

template <typename A, typename B, typename = meta_enable_if_expr<A>> 
inline auto operator+(A&& a, B&& b) 
{ 
    return add_expr<A, B>{ std::forward<A>(a), std::forward<B>(b) }; 
} 

이 우리에 대한 식 트리를 구축하는 컴파일러를 강제 할 책임이있다. sreg에 + 연산자를 오버로드하고 데이터가 sreg 인 것처럼 반복하면 컴파일러는 sreg__m128d을 나타내는 sreg 내장 래퍼에서 연산자로 식을 인라인합니다.

표현 expr의 각 특수화는 sreg 이상의 기능을합니다. 방금 테스트 목적으로 expr_add을 구현했습니다.

+0

나는'__m128' /'__m128i에 대한 래퍼를 보았다. '/'__m256 ...'연산자 (예 : Agner Fog의 GPL 라이센스 [vectorclass C++ templates] (http://www.agner.org/optimize/#vectorclass))와 operator + 등등이 있지만 이전에는 볼 수 없었습니다. 전에는 임의의 길이의 컨테이너를위한 래퍼. 각 연산자에 대해 별도의 루프를 만들지 않도록하기가 쉽지 않기 때문일 수 있습니다. 'a + b'를 저장하고, 'c'로부터 벡터를 추가하면서 다시 읽어들입니다. –

+0

저는 템플릿 마법사가 아니므로 실제로 모든 작업이 하나의 루프에서 이루어 지도록 조치를 취하고 있습니까? 그렇다면 어떻게 그렇게 할 수 있을지 생각해보십시오. 신뢰성이 있다면 잠재적으로 유용 할 것입니다. –

+1

@Peter Cordes, 추가 정보를 추가 했으므로 여러 "배열"또는 표현식에서 작동 할 때만 루프 하나만 얻게됩니다. 'sreg'는 Agner Fog가 제작 한 것과 유사합니다. – keith