개발관련/other

잘 작성된 C 코드보다 어셈블리가 더 빠른 경우의 예

Rateye 2021. 11. 10. 10:40
728x90
반응형
질문 : 조립이 C보다 빠른 때는 언제입니까?

어셈블러를 아는 이유 중 하나는 때때로 더 높은 수준의 언어, 특히 C로 코드를 작성하는 것보다 더 성능이 좋은 코드를 작성하는 데 사용될 수 있다는 것입니다. 그러나 전적으로 잘못된 것은 아니지만 어셈블러를 실제로 사용하여 더 많은 성능의 코드를 생성 할 수있는 경우는 극히 드물고 어셈블리에 대한 전문 지식과 경험이 필요하다고 여러 번 언급 한 적이 있습니다.

이 질문은 어셈블러 명령어가 기계별로 다르고 이식 불가능하다는 사실이나 어셈블러의 다른 측면에 대해서도 설명하지 않습니다. 물론이 외에도 어셈블리를 아는 데는 많은 이유가 있지만, 이것은 어셈블러 대 상위 수준 언어에 대한 확장 된 담론이 아니라 예제와 데이터를 요청하는 특정 질문을 의미합니다.

누구나 최신 컴파일러를 사용하여 잘 작성된 C 코드보다 어셈블리가 더 빠른 경우의 특정 예 를 제공 할 수 있으며 프로파일 링 증거로 해당 주장을 지원할 수 있습니까? 나는이 사건들이 존재한다고 확신하지만,이 사건들이 얼마나 난해한 지 정확히 알고 싶습니다. 왜냐하면 그것이 어떤 논쟁의 지점 인 것 같기 때문입니다.

답변

다음은 실제 사례입니다. 고정 소수점은 오래된 컴파일러에서 곱합니다.

이는 부동 소수점이없는 장치에서만 유용 할뿐만 아니라 예측 가능한 오류와 함께 32 비트의 정밀도를 제공하기 때문에 정밀도 측면에서 빛을 발합니다 (float는 23 비트 만 있고 정밀도 손실을 예측하기가 더 어렵습니다). 즉, 거의 균일 한 상대 정밀도 ( float ) 대신 전체 범위에 걸쳐 균일 한 절대 정밀도.

최신 컴파일러는이 고정 소수점 예제를 멋지게 최적화하므로 컴파일러 별 코드가 필요한 최신 예제는 다음을 참조하십시오.

  • 64 비트 정수 곱셈의 높은 부분 가져 오기 : 32x32 => 64 비트 곱셈에 대해 uint64_t 를 사용하는 휴대용 버전은 64 비트 CPU에서 최적화되지 않으므로 64 비트 시스템에서 효율적인 코드 __int128
  • _umul128 on Windows 32 bits : MSVC는 32 비트 정수를 64로 캐스트 할 때 항상 좋은 작업을 수행하지 않으므로 intrinsics가 많은 도움이되었습니다.

C에는 완전 곱셈 연산자가 없습니다 (N 비트 입력의 2N 비트 결과). C로 표현하는 일반적인 방법은 입력을 더 넓은 유형으로 캐스트하고 컴파일러가 입력의 상위 비트가 흥미롭지 않다는 것을 인식하기를 바랍니다.

// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
  long long a_long = a; // cast to 64 bit.

  long long product = a_long * b; // perform multiplication

  return (int) (product >> 16);  // shift by the fixed point bias
}

이 코드의 문제점은 C 언어로 직접 표현할 수없는 일을한다는 것입니다. 우리는 두 개의 32 비트 숫자를 곱하고 64 비트 결과를 얻고 자하며 그 결과 중간 32 비트를 반환합니다. 그러나 C에서는이 곱셈이 존재하지 않습니다. 할 수있는 일은 정수를 64 비트로 승격하고 64 * 64 = 64 곱하기를 수행하는 것입니다.

그러나 x86 (및 ARM, MIPS 및 기타)은 단일 명령어에서 곱하기를 수행 할 수 있습니다. 일부 컴파일러는이 사실을 무시하고 곱하기를 수행하기 위해 런타임 라이브러리 함수를 호출하는 코드를 생성하는 데 사용되었습니다. 16 씩 시프트는 종종 라이브러리 루틴에 의해 수행됩니다 (x86도 이러한 시프트를 수행 할 수 있음).

그래서 우리는 단지 곱하기 위해 하나 또는 두 개의 라이브러리 호출이 남습니다. 이것은 심각한 결과를 초래합니다. 시프트가 느릴뿐만 아니라 레지스터는 함수 호출에서 보존되어야하며 인라인 및 코드 언 롤링에도 도움이되지 않습니다.

(인라인) 어셈블러에서 동일한 코드를 다시 작성하면 상당한 속도 향상을 얻을 수 있습니다.

또한 ASM을 사용하는 것이 문제를 해결하는 가장 좋은 방법은 아닙니다. 대부분의 컴파일러에서는 C로 표현할 수없는 경우 일부 어셈블러 명령어를 내장 형식으로 사용할 수 있습니다. 예를 들어 VS.NET2008 컴파일러는 32 * 32 = 64 비트 mul을 __emul로 노출하고 64 비트 시프트를 __ll_rshift로 노출합니다.

내장 함수를 사용하면 C- 컴파일러가 무슨 일이 일어나고 있는지 이해할 수있는 방식으로 함수를 다시 작성할 수 있습니다. 이를 통해 코드를 인라인하고, 레지스터를 할당하고, 공통 하위 표현식을 제거하고, 상수 전파를 수행 할 수도 있습니다. 그렇게하면 손으로 작성한 어셈블러 코드에 비해 엄청난 성능 향상을 얻을 수 있습니다.

참고로 : VS.NET 컴파일러의 고정 소수점 mul에 대한 최종 결과는 다음과 같습니다.

int inline FixedPointMul (int a, int b)
{
    return (int) __ll_rshift(__emul(a,b),16);
}

고정 소수점 분할의 성능 차이는 훨씬 더 큽니다. 몇 개의 asm-line을 작성하여 나누기 무거운 고정 소수점 코드에 대해 요소 10까지 개선했습니다.

Visual C ++ 2013을 사용하면 두 가지 방법에 대해 동일한 어셈블리 코드가 제공됩니다.

2007의 gcc4.1은 또한 순수 C 버전을 멋지게 최적화합니다. (Godbolt 컴파일러 탐색기에는 이전 버전의 gcc가 설치되어 있지 않지만 아마도 이전 GCC 버전에서도 내장 함수없이이를 수행 할 수 있습니다.)

Godbolt 컴파일러 탐색기 에서 x86 (32 비트) 및 ARM 용 source + asm을 참조하십시오. (안타깝게도 단순한 순수 C 버전에서 잘못된 코드를 생성 할 수있는 오래된 컴파일러가 없습니다.)

같은 현대 CPU는 전혀 C가 사업자가없는 일을 할 수 popcnt 또는 비트 검사가 첫 번째 또는 마지막 세트 비트를 찾을 수 있습니다. (POSIX에는 ffs() 함수가 있지만 그 의미는 x86 bsf / bsr 과 일치하지 않습니다. https://en.wikipedia.org/wiki/Find_first_set 참조 ).

일부 컴파일러는 정수에서 설정된 비트 수를 계산하는 루프를 인식하고이를 popcnt 명령어 (컴파일시 활성화 된 경우)로 컴파일 할 수 있지만, GNU C에서 __builtin_popcnt _mm_popcnt_u32 from <immintrin.h> 하는 하드웨어 만 대상으로합니다.

또는 C ++에서 std::bitset<32> .count() 합니다. (이것은 언어가 표준 라이브러리를 통해 popcount의 최적화 된 구현을 이식 가능하게 노출하는 방법을 찾은 경우입니다. 항상 올바른 것으로 컴파일되고 대상이 지원하는 모든 것을 활용할 수 있습니다.) https도 참조하십시오. : //en.wikipedia.org/wiki/Hamming_weight#Language_support

마찬가지로 ntohl bswap 하는 일부 C 구현에서 bswap (엔디안 변환을위한 x86 32 비트 바이트 스왑)으로 컴파일 할 수 있습니다.

내장 함수 또는 손으로 작성한 asm의 또 다른 주요 영역은 SIMD 명령어를 사용한 수동 벡터화입니다. 컴파일러는 dst[i] += src[i] * 10.0; ,하지만 상황이 더 복잡해지면 종종 잘못하거나 자동 벡터화하지 않습니다. 예를 들어 SIMD를 사용하여 atoi를 구현하는 방법 과 같은 것을 얻을 가능성은 거의 없습니다. 스칼라 코드에서 컴파일러에 의해 자동으로 생성됩니다.

출처 : https://stackoverflow.com/questions/577554/when-is-assembly-faster-than-c
728x90
반응형