어셈블리가 C보다 빠른 경우는 언제입니까?


475

어셈블러를 알고있는 이유 중 하나는 경우에 따라 코드를 특히 고급 언어 인 C로 작성하는 것보다 성능이 좋은 코드를 작성하는 데 사용될 수 있기 때문입니다. 그러나 나는 그것이 완전히 잘못된 것은 아니지만 어셈블러를 사용하여 실제로 더 성능이 좋은 코드를 생성 하는 경우가 매우 드물고 어셈블리에 대한 전문 지식과 경험이 필요 하다고 여러 번 들었습니다 .

이 질문은 어셈블러 명령어가 기계별로 다르고 이식 가능하지 않거나 어셈블러의 다른 측면 중 하나라는 사실조차 알지 못합니다. 물론 이것 외에 어셈블리를 알아야 할 충분한 이유가 많이 있지만, 이것은 어셈블러와 고급 언어에 대한 확장 된 담론이 아니라 예제와 데이터를 요구하는 특정 질문을 의미합니다.

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


17
실제로 컴파일 된 코드를 개선하는 것은 매우 사소한 일입니다. 어셈블리 언어와 C에 대해 잘 알고있는 사람은 생성 된 코드를 검사하여이를 확인할 수 있습니다. 쉬운 방법은 컴파일 된 버전에서 일회용 레지스터가 부족할 때 빠지는 첫 번째 성능 절벽입니다. 평균적으로 컴파일러는 대규모 프로젝트의 경우 사람보다 훨씬 나을 것이지만 적절한 크기의 프로젝트에서는 컴파일 된 코드에서 성능 문제를 찾는 것이 어렵지 않습니다.
old_timer

14
사실, 짧은 대답은 다음과 같습니다. 어셈블러는 항상 C의 속도와 같거나 빠릅니다. 그 이유는 C없이 어셈블리를 가질 수 있지만 어셈블리없이 C를 가질 수는 없기 때문입니다 (이전 형식의 이진 형식). "기계 코드"라고하는 날). C 컴파일러는 일반적으로 생각하지 않는 것에 대해 최적화하고 "생각"하는 데 능숙하므로 실제로 기술에 달려 있지만 일반적으로 항상 C 컴파일러를 이길 수 있습니다. 여전히 생각하고 아이디어를 얻을 수없는 소프트웨어 일뿐입니다. 매크로를 사용하고 인내심을 가지고 있다면 휴대용 어셈블러를 작성할 수도 있습니다.

11
나는이 질문에 대한 답변이 "의견에 근거한 것"이어야한다는 것에 동의하지 않는다.-그것들은 상당히 객관적 일 수있다-각각의 장점과 단점이있는, 좋아하는 애완 동물 언어의 성능을 비교하려고하는 것과는 다르다. 이것은 컴파일러가 얼마나 멀리 우리를 데려 갈 수 있는지 이해하는 것입니다.
jsbueno

21
경력 초기에는 소프트웨어 회사에서 C 및 메인 프레임 어셈블러를 많이 작성했습니다. 내 동료 중 하나는 내가 "어셈블러 순수 주의자"(모든 것이 어셈블러 여야 함)라고 부르는 것이므로 C에서 더 빨리 실행되는 주어진 루틴을 어셈블러에서 작성할 수있는 것보다 더 쓸 수 있다고 내기했습니다. 내가이 겄어. 그러나 내가 이기고 난 후에, 나는 그에게 두 번째 베팅을 원한다고 말했다. 나는 이전 베팅에서 그를이기는 C 프로그램보다 더 빨리 무언가를 어셈블러로 작성할 수 있다고 말했다. 나도 이겼다. 그것의 대부분은 다른 무엇보다도 프로그래머의 기술과 능력에 달려있다.
Valerie R

3
두뇌에 -O3플래그 가 없다면 C 컴파일러에 최적화를 두는 것이 좋습니다. :-)
paxdiablo

답변:


272

실제 예는 다음과 같습니다. 고정 소수점은 이전 컴파일러에서 곱합니다.

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


최신 컴파일러는이 고정 소수점 예제를 훌륭하게 최적화하므로 여전히 컴파일러 관련 코드가 필요한 최신 예제는 다음을 참조하십시오.

  • 64 비트 정수 곱셈의 많은 부분 얻기 : uint64_t32x32 => 64 비트 곱하기에 사용하는 이식 가능한 버전 은 64 비트 CPU에서 최적화하지 못하므로 __int12864 비트 시스템의 내장 코드 나 효율적인 코드 가 필요 합니다.
  • Windows 32 비트의 _umul128 : 32 비트 정수를 64로 캐스트 할 때 MSVC가 항상 잘 작동하지 않으므로 내장 함수가 많은 도움이되었습니다.

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명령으로 컴파일하는 루프를 인식 할 수 있지만 (컴파일 타임에 활성화 된 경우) __builtin_popcntGNU C 또는 x86에서만 사용하는 것이 훨씬 안정적입니다. SSE4.2와 하드웨어를 대상으로 : _mm_popcnt_u32에서<immintrin.h> .

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

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


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


6
{x = c % d; y = c / d;}, 컴파일러가 단일 div 또는 idiv를 만들 정도로 영리합니까?
Jens Björnhager

4
실제로 좋은 컴파일러는 첫 번째 함수에서 최적의 코드를 생성합니다. 본질적으로 또는 인라인 어셈블리 소스 코드를 가려서 아무런 이점 이없는 것은 최선의 방법이 아닙니다.
slacker

65
안녕하세요 슬랙 커, 인라인 어셈블리가 큰 차이를 만들 수 있기 전에 시간이 중요한 코드 작업을 한 번도 해본 적이 없다고 생각합니다. 또한 컴파일러의 내장 함수는 C의 일반 산술과 동일합니다. 이것이 내장 함수의 요점입니다. 이를 통해 단점을 처리하지 않고도 아키텍처 기능을 사용할 수 있습니다.
Nils Pipenbrinck

6
@slacker 실제로, 여기의 코드는 아주 읽기 쉽다 : 인라인 코드는 하나의 고유 한 연산을 수행하는데, 이는 메소드 서명을 읽는 즉시 불안정합니다. 불분명 한 명령을 사용하면 코드의 가독성이 느려집니다. 여기서 중요한 것은 명확하게 식별 가능한 작업을 하나만 수행하는 방법이며, 이러한 원자 함수를 읽을 수있는 코드를 생성하는 가장 좋은 방법입니다. 그건 그렇고, 이것은 / * (a * b) >> 16 * /와 같은 작은 설명을 모호하지는 않지만 즉시 설명 할 수는 없습니다.
Dereckson

5
공평하게 말하면, 이것은 적어도 오늘은 나쁜 예입니다. C 컴파일러는 언어가 직접 제공하지 않더라도 32x32-> 64 곱하기를 오랫동안 수행해 왔습니다. 32 비트 인수를 64 비트로 캐스팅 한 다음 곱할 때 전체 64 비트 곱하기를 수행하지만 32x32-> 64는 정상적으로 작동합니다. 나는 현재 버전clang, gcc 및 MSVC를 모두 올바르게 검사했다 . 이것은 새로운 것이 아닙니다. 10 년 전 컴파일러 출력을보고이를 알아 차린 기억이납니다.
BeeOnRope 3:27에

143

몇 년 전에 나는 누군가에게 C로 프로그램하도록 가르치고 있었다. 운동은 그래픽을 90도 회전시키는 것이었다. 그는 곱셈과 나누기 등을 사용했기 때문에 완료하는 데 몇 분이 걸리는 솔루션으로 돌아 왔습니다.

비트 시프트를 사용하여 문제를 해결하는 방법을 보여 주었고 처리 시간은 최적화되지 않은 컴파일러에서 약 30 초로 줄었습니다.

방금 최적화 컴파일러를 얻었고 동일한 코드로 그래픽을 <5 초 만에 회전했습니다. 나는 컴파일러가 생성하고있는 어셈블리 코드를 살펴 보았고, 내가 본 것으로 결정한 다음 어셈블러 작성의 시대는 끝났다.


3
그렇습니다. 이것은 1 비트 흑백 시스템이었습니다. 특히 Atari ST의 흑백 이미지 블록이었습니다.
lilburne

16
최적화 컴파일러가 원래 프로그램 또는 버전을 컴파일 했습니까?
Thorbjørn Ravn Andersen

어떤 프로세서에서? 8086에서 8x8 회전을위한 최적의 코드는 SI를 사용하여 16 비트의 데이터로 DI를로드 add di,di / adc al,al / add di,di / adc ah,ah하고 8 개의 8 비트 레지스터 모두에 대해 반복 등을 수행 한 다음 8 개의 레지스터를 모두 다시 수행 한 다음 전체 절차 3을 반복 할 것으로 예상합니다 더 많은 시간을 보내고 마지막으로 ax / bx / cx / dx에 네 단어를 저장하십시오. 어셈블러가 그것에 가까이 갈 수는 없습니다.
supercat

1
나는 컴파일러가 8x8 회전을위한 최적의 코드 중 2 ~ 2 배 안에 들어갈 수있는 플랫폼을 생각할 수 없습니다.
supercat

65

컴파일러가 부동 소수점 코드를 볼 때마다 오래된 나쁜 컴파일러를 사용하는 경우 수작업으로 작성된 버전이 더 빠릅니다. ( 2019 업데이트 : 이것은 현대 컴파일러의 경우 일반적으로 사실이 아닙니다. 특히 x87 이외의 것을 컴파일 할 때; 스칼라 연산을 위해 SSE2 또는 AVX 또는 x87과 달리 플랫 FP 레지스터 세트가있는 x86이 아닌 컴파일러를 사용하면 컴파일러가 더 쉬워집니다 레지스터 스택.)

주된 이유는 컴파일러가 강력한 최적화를 수행 할 수 없기 때문입니다. 주제에 대한 토론 은 MSDN의이 기사를 참조하십시오 . 다음은 어셈블리 버전이 C 버전보다 2 배 빠른 속도 (VS2K5로 컴파일 된)의 예입니다.

#include "stdafx.h"
#include <windows.h>

float KahanSum(const float *data, int n)
{
   float sum = 0.0f, C = 0.0f, Y, T;

   for (int i = 0 ; i < n ; ++i) {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum(const float *data, int n)
{
  float result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int count = 1000000;

  float *source = new float [count];

  for (int i = 0 ; i < count ; ++i) {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER start, mid, end;

  float sum1 = 0.0f, sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

그리고 내 PC의 일부 숫자는 기본 릴리스 빌드 *를 실행합니다 .

  C code: 500137 in 103884668
asm code: 500137 in 52129147

흥미롭게도 루프를 dec / jnz로 바꾸었고 타이밍에 아무런 영향을 미치지 않았습니다. 때로는 더 빠르거나 때로는 느립니다. 메모리 제한 측면이 다른 최적화를 방해한다고 생각합니다. (편집자 주 : FP 대기 시간 병목 현상은 추가 비용을 감추기에 충분할 가능성이 높습니다 loop. 홀수 / 짝수 요소에 대해 두 개의 Kahan 합계를 병렬로 수행하고 마지막에이를 추가하면 속도가 2 배 증가 할 수 있습니다. )

나는 약간 다른 버전의 코드를 실행하고 있었고 숫자를 잘못된 방식으로 출력했습니다 (즉, C가 빠릅니다!). 결과를 수정하고 업데이트했습니다.


20
또는 GCC에서는 플래그를 사용하여 컴파일러에서 부동 소수점 최적화에 대한 손을 풀 수 있습니다 (무한 또는 NaN을 사용하지 않는 것을 약속하는 한) -ffast-math. 그것들 -Ofast은 현재와 동등한 최적화 수준을 가지고 -O3 -ffast-math있지만, 앞으로는 IEEE NaN에 의존하는 코드와 같은 코너 코드에서 잘못된 코드 생성을 초래할 수있는 더 많은 최적화가 포함될 수 있습니다.
David Stone

2
예, 플로트는 정식이 아니며 컴파일러는 기본적으로 @DavidStone이 말한 것을 정확하게 작성해야합니다.
Alec Teal

2
SSE 수학을 시도 했습니까? 성능은 MS가 x86_64에서 x87을 완전히 포기하고 x86에서 80 비트 롱 더블을 포기한 이유 중 하나입니다.
phuclv

4
@Praxeolitic : FP add는 정식 ( a+b == b+a)이지만 연관되지는 않습니다 (작업 순서 변경, 중간 반올림이 다름). re :이 코드 : 주석 처리되지 않은 x87과 loop명령어는 빠른 asm의 매우 멋진 데모 라고 생각하지 않습니다 . loopFP 대기 시간으로 인해 실제로 병목 현상이 아닙니다. 그가 FP 작업을 파이프 라이닝하고 있는지 잘 모르겠습니다. x87은 인간이 읽기 어렵습니다. fstp results마지막에 두 개의 여관은 분명히 최적이 아닙니다. 스택에서 추가 결과를 가져 오는 것은 비 스토어를 사용하는 것이 좋습니다. fstp st(0)IIRC 처럼 .
Peter Cordes

2
@PeterCordes : 덧셈을 반복하면 흥미로운 결과는 0 + x와 x ​​+ 0이 서로 같지만 항상 x와 같지 않다는 것입니다.
supercat

58

구체적인 예나 프로파일 러 증거를 제공하지 않으면 컴파일러보다 더 많은 것을 알고있을 때 컴파일러보다 더 나은 어셈블러를 작성할 수 있습니다.

일반적으로 최신 C 컴파일러는 문제의 코드를 최적화하는 방법에 대해 훨씬 더 많이 알고 있습니다. 프로세서 파이프 라인의 작동 방식을 알고 있으며 사람보다 명령을 더 빨리 재정렬하려고 할 수 있습니다. 컴퓨터는 대부분의 인간보다 문제 공간 내에서 더 빨리 검색 할 수 있기 때문에 보드 게임 등 최고의 인간 플레이어보다 우수하거나 더 우수합니다. 이론적으로 특정 경우에 컴퓨터만큼 성능을 ​​발휘할 수 있지만 동일한 속도로 수행 할 수 없어서 몇 가지 이상의 경우에는 불가능합니다 (예를 들어, 작성하려고하면 컴파일러가 가장 확실히 성능을 발휘합니다) 어셈블러의 몇 가지 루틴 이상).

반면에 컴파일러에 많은 정보가없는 경우가 있습니다. 주로 다른 형식의 외부 하드웨어로 작업 할 때 컴파일러에 대한 지식이 없습니다. 주요 예제는 아마도 장치 드라이버 일 것입니다. 어셈블러는 문제의 하드웨어에 대한 인간의 친밀한 지식과 결합하여 C 컴파일러보다 더 나은 결과를 얻을 수 있습니다.

다른 사람들은 위의 단락에서 말하고있는 특수 목적 명령어에 대해 언급했습니다. 컴파일러는 지식이 전혀 없거나 전혀 없기 때문에 인간이 더 빠른 코드를 작성할 수 있습니다.


일반적으로이 진술은 사실입니다. 컴파일러는 DWIW에 가장 적합하지만 일부 경우에는 실시간 성능이 필요한 경우 수동 코딩 어셈블러가 작업을 수행합니다.
spoulson

1
@Liedman : "인간보다 명령을 빨리 재주문 할 수 있습니다." OCaml은 빠르다는 것으로 유명하며 놀랍게도 네이티브 코드 컴파일러 ocamlopt는 x86에서 명령어 스케줄링을 건너 뛰고 대신 런타임에보다 효과적으로 재정렬 할 수 있기 때문에 CPU에 맡깁니다.
Jon Harrop

1
현대 컴파일러는 많은 작업을 수행하며 수동으로 수행하는 데 시간이 너무 오래 걸리지 만 완벽하지는 않습니다. "누락 된 최적화"버그에 대해 gcc 또는 llvm의 버그 추적기를 검색하십시오. 많이있다. 또한 asm으로 작성할 때 컴파일러가 증명하기 어려운 "이 입력은 음수 일 수 없습니다"와 같은 사전 조건을보다 쉽게 ​​활용할 수 있습니다.
Peter Cordes

48

직장에서 어셈블리를 알고 사용해야하는 세 가지 이유가 있습니다. 중요하게 :

  1. 디버깅-버그 나 불완전한 문서가 포함 된 라이브러리 코드가 자주 나타납니다. 어셈블리 수준에서 시작하여 수행중인 작업을 파악합니다. 일주일에 한 번 정도해야합니다. 또한 C / C ++ / C #에서 내 눈이 관용적 오류를 발견하지 못하는 문제를 디버깅하는 도구로 사용합니다. 어셈블리를 보면 그게지나갑니다.

  2. 최적화-컴파일러는 최적화에서 상당히 잘 수행하지만 대부분 다른 야구장에서 경기합니다. 일반적으로 다음과 같은 코드로 시작하는 이미지 처리 코드를 작성합니다.

    for (int y=0; y < imageHeight; y++) {
        for (int x=0; x < imageWidth; x++) {
           // do something
        }
    }

    "일부 수행"은 일반적으로 수백만 번 (즉, 3에서 30 사이) 발생합니다. 그 "무언가"단계에서 사이클을 폐기함으로써 성능 향상이 크게 확대됩니다. 나는 보통 거기에서 시작하지 않습니다-나는 보통 먼저 작동하도록 코드를 작성하여 시작한 다음 C를 리팩터링하여 더 나은 알고리즘으로 만들려고 최선을 다합니다 (더 나은 알고리즘, 루프의 적은 부하 등). 나는 보통 무슨 일이 일어나고 있는지 알기 위해 어셈블리를 읽을 필요가 있으며 거의 ​​쓸 필요가 없습니다. 아마 2 ~ 3 개월마다 이렇게합니다.

  3. 언어로 할 수없는 일을합니다. 여기에는 프로세서 아키텍처 및 특정 프로세서 기능 가져 오기, CPU에없는 플래그 액세스 (남자, C가 캐리 플래그에 대한 액세스 권한을 부여 했음) 등이 포함됩니다. 1 년 또는 2 년에 한 번 정도 수행합니다.


당신은 당신의 루프를 타일하지 않습니다? :-)
Jon Harrop

1
@ 주각 : "스크래핑 사이클"을 어떻게 의미합니까?
lang2

@ lang2 : 내부 루프에서 가능한 많은 불필요한 시간을 없애는 것을 의미합니다. 컴파일러가 꺼내지 않은 모든 것-대수를 사용하여 하나의 루프에서 곱하기를 추가하여 추가 할 수 있음 안 등에서
주각

1
데이터를 한 번만 통과하는 경우 루프 타일링이 필요하지 않은 것 같습니다.
James M. Lay

@ JamesM.Lay : 모든 요소를 ​​한 번만 터치하면 순회 순서가 좋을수록 공간적 위치를 지정할 수 있습니다. (예 : 캐시 라인 당 하나의 요소를 사용하여 행렬의 열을 반복하는 대신 터치 한 캐시 라인의 모든 바이트를 사용하십시오.)
Peter Cordes

42

특수 목적 명령어 세트를 사용하는 경우에만 컴파일러가 지원하지 않습니다.

여러 파이프 라인과 예측 분기를 사용하여 최신 CPU의 컴퓨팅 성능을 극대화하려면 a) 사람이 작성하는 것이 거의 불가능한 방식으로 조립 프로그램을 구성해야합니다. b) 유지 관리가 훨씬 불가능합니다.

또한 더 나은 알고리즘, 데이터 구조 및 메모리 관리를 통해 어셈블리에서 수행 할 수있는 미세 최적화보다 훨씬 더 많은 성능을 얻을 수 있습니다.


4
+1, 마지막 문장이이 논의에 실제로 속하지는 않지만 알고리즘 등의 모든 가능한 개선이 실현 된 후에 만 ​​어셈블러가 작동한다고 가정합니다.
mghie

18
@Matt : 손으로 쓴 ASM은 종종 크 래피 한 벤더 컴파일러를 지원하는 EE의 작은 CPU에서 훨씬 나을 수 있습니다.
Zan Lynx

5
"특수 목적 명령어 세트를 사용할 때만"?? 당신은 아마 손으로 최적화 된 asm 코드를 작성하지 않았을 것입니다. 작업중인 아키텍처에 대한 적절한 지식이 있으면 컴파일러보다 더 나은 코드 (크기 및 속도)를 생성 할 수 있습니다. 분명히 @mghie가 언급했듯이 항상 문제를 해결할 수있는 최상의 알고리즘을 코딩하기 시작합니다. 매우 우수한 컴파일러의 경우에도 컴파일러를 최상의 컴파일 코드로 이끄는 방식으로 C 코드를 작성해야합니다. 그렇지 않으면 생성 된 코드가 차선책이됩니다.
ysap

2
@ysap-실제 사용시 실제 컴퓨터 (작은 저전력 임베디드 칩이 아님)에서 "최적의"코드는 더 빠르지 않습니다. 큰 데이터 세트의 경우 메모리 액세스 및 페이지 오류로 인해 성능이 제한되기 때문입니다 ( 그리고 큰 데이터 세트가 없다면 이것은 어느 쪽이든 빠르며 최적화 할 필요가 없습니다.)-나는 주로 C # (c조차 아님)에서 일하고 압축 메모리 관리자의 성능 향상 가비지 콜렉션, 압축 및 JIT 컴파일의 오버 헤드에 가중치를 둡니다.
Nir

4
컴파일러 (esp. JIT) 가 실행되는 하드웨어에 최적화 된 경우 컴파일러 보다 사람 이 더 나은 작업을 수행 할 수 있다고 +1합니다 .
Sebastian

38

C가 8 비트, 16 비트, 32 비트, 64 비트 데이터의 하위 수준 조작에 "가까운"경우에도 C에서 지원하지 않는 일부 수학적 연산이있어 특정 조립 명령에서 우아하게 수행 할 수 있습니다. 세트 :

  1. 고정 소수점 곱셈 : 두 개의 16 비트 숫자의 곱은 32 비트 숫자입니다. 그러나 C의 규칙에 따르면 두 개의 16 비트 숫자의 곱은 16 비트 숫자이고 두 개의 32 비트 숫자의 곱은 32 비트 숫자입니다 (두 경우 모두 아래쪽 절반). 당신이 원한다면 상단의 곱셈 곱셈 16 × 16 또는 32 × 32의 절반을, 당신은 컴파일러와 놀이 게임에 있습니다. 일반적인 방법은 필요한 것보다 큰 비트 폭으로 캐스트하고, 곱하고, 아래로 내리고, 캐스트하는 것입니다.

    int16_t x, y;
    // int16_t is a typedef for "short"
    // set x and y to something
    int16_t prod = (int16_t)(((int32_t)x*y)>>16);`

    이 경우 컴파일러는 실제로 16x16 배수의 상위 절반을 얻고 기계의 기본 16x16multiply로 올바른 일을하려고한다는 것을 알기에 똑똑 할 수 있습니다. 또는 어리 석고 32x32 곱하기를 수행하려면 라이브러리 호출이 필요합니다 .16 비트의 제품 만 필요하기 때문에 과잉입니다. 그러나 C 표준은 자신을 표현할 수있는 방법을 제공하지 않습니다.

  2. 특정 비트 시프 팅 작업 (회전 / 운반) :

    // 256-bit array shifted right in its entirety:
    uint8_t x[32];
    for (int i = 32; --i > 0; )
    {
       x[i] = (x[i] >> 1) | (x[i-1] << 7);
    }
    x[0] >>= 1;

    이것은 C에서 너무 우아하지는 않지만 컴파일러가 당신이하고있는 일을 깨닫기에 충분하지 않다면 많은 "불필요한"작업을 할 것입니다. 많은 어셈블리 명령어 세트를 사용하면 캐리 레지스터의 결과와 함께 왼쪽 / 오른쪽으로 회전하거나 이동할 수 있으므로 위의 34 개 명령어로 위의 작업을 수행 할 수 있습니다. 어레이의 시작 부분에 대한 포인터로드, 캐리 지우기 및 수행 32 8- 포인터의 자동 증가를 사용하여 비트 오른쪽 이동.

    또 다른 예로, 어셈블리에서 우아하게 수행 되는 선형 피드백 시프트 레지스터 (LFSR)가 있습니다. N 비트 청크 (8, 16, 32, 64, 128 등)를 취하고 전체를 1 씩 오른쪽으로 시프트하십시오 (위 참조). 알고리즘), 결과 캐리가 1이면 다항식을 나타내는 비트 패턴으로 XOR됩니다.

그러나 성능에 심각한 제약이 없다면 이러한 기술에 의존하지 않을 것입니다. 다른 사람들이 말했듯이 어셈블리는 C 코드보다 문서화 / 디버그 / 테스트 / 유지 관리가 훨씬 어렵습니다. 성능 향상에는 약간의 비용이 따릅니다.

편집 : 3. 어셈블리에서 오버플로 감지가 가능합니다 (실제로 C에서는 할 수 없음). 이는 일부 알고리즘을 훨씬 쉽게 만듭니다.


23

짧은 답변? 때때로.

기술적으로 모든 추상화에는 비용이 있으며 프로그래밍 언어는 CPU 작동 방식에 대한 추상화입니다. 그러나 C는 매우 가깝습니다. 몇 년 전 유닉스 계정에 로그인하여 다음과 같은 행운의 메시지를 받았습니다.

C 프로그래밍 언어-어셈블리 언어의 유연성과 어셈블리 언어의 힘을 결합한 언어입니다.

C가 이식 가능한 어셈블리 언어와 같습니다.

어셈블리 언어는 실행되지만 실제로 작성한다는 점은 주목할 가치가 있습니다. 그러나 C와 C가 생성하는 어셈블리 언어 사이에 컴파일러 가 있으며 C 코드의 속도가 컴파일러의 우수성과 많은 관련이 있기 때문에 매우 중요 합니다.

gcc가 등장했을 때 인기를 끌었던 것 중 하나는 많은 상용 UNIX 풍미와 함께 제공되는 C 컴파일러보다 훨씬 낫다는 것입니다. ANSI C (이 K & R C 쓰레기는 아님) 일뿐만 아니라 더욱 강력하고 일반적으로 더 나은 (더 빠른) 코드를 생산했습니다. 항상 그렇지는 않지만 자주.

C에 대한 객관적인 표준이 없기 때문에 C와 어셈블러의 속도에 대한 담요 규칙이 없기 때문에이 모든 것을 말해줍니다.

마찬가지로 어셈블러는 실행중인 프로세서, 시스템 사양, 사용중인 명령어 세트 등에 따라 크게 달라집니다. 역사적으로 CISC와 RISC의 두 가지 CPU 아키텍처 제품군이있었습니다. CISC의 가장 큰 플레이어는 인텔 x86 아키텍처 (및 명령어 세트)였습니다. RISC는 UNIX 세계 (MIPS6000, Alpha, Sparc 등)를 지배했습니다. CISC는 마음과 마음을위한 전투에서 승리했습니다.

어쨌든, 제가 젊은 개발자 였을 때의 대중적인 지혜는 수작업으로 작성된 x86이 종종 C보다 훨씬 빠를 수 있다는 것입니다. 아키텍처가 작동하는 방식에는 사람이 작업을 수행하는 데 따른 복잡성이 있었기 때문입니다. 반면에 RISC는 컴파일러를 위해 설계된 것처럼 보였으므로 Sparc 어셈블러를 쓴 사람은 아무도 없었습니다. 나는 그런 사람들이 존재했다고 확신하지만 의심 할 여지없이 그들은 지금 미쳤고 제도화되었습니다.

명령어 세트는 동일한 프로세서 제품군에서도 중요한 포인트입니다. 특정 인텔 프로세서에는 SSE에서 SSE4와 같은 확장 기능이 있습니다. AMD는 자체 SIMD 명령을 가지고있었습니다. C와 같은 프로그래밍 언어의 장점은 누군가 라이브러리를 작성할 수있어 실행중인 프로세서에 최적화되어 있습니다. 그것은 어셈블러에서 열심히 일했습니다.

아직 컴파일러가 만들 수없는 어셈블러에서 최적화 할 수 있으며 잘 작성된 어셈블러 알고리즘은 C에 비해 빠르거나 빠릅니다. 더 큰 문제는 가치가 있습니까?

궁극적으로 어셈블러는 당시의 산물 이었지만 CPU 사이클이 비쌀 때 더 인기가있었습니다. 오늘날 5-10 달러의 비용이 드는 CPU (Intel Atom)는 누구나 원하는 모든 것을 할 수 있습니다. 요즘 어셈블러를 작성하는 유일한 이유는 운영 체제의 일부 (리눅스 커널의 대부분이 C로 작성 됨), 장치 드라이버, 내장형 장치 (C가 지배적 인 경향이 있음)와 같은 저수준의 것입니다. 너무). 또는 차기 (약간의 masochistic).


Acorn 머신 (90 년대 초)에서 ARM 어셈블러를 선택한 언어로 사용하는 사람들이 많이있었습니다. IIRC 그들은 작은 늑골 지시 세트가 더 쉽고 재미있게 만들었다 고 말했다. 그러나 C 컴파일러가 Acorn에 늦게 도착했기 때문에 C ++ 컴파일러가 끝나지 않았기 때문입니다.
앤드류 M

3
"... C에 대한 주관적인 표준이 없기 때문에." 당신은 객관적인 것을 의미 합니다.
Thomas

@AndrewM : 예, 저는 약 10 년 동안 BASIC 및 ARM 어셈블러에서 혼합 언어 응용 프로그램을 작성했습니다. 나는 그 시간 동안 C를 배웠지 만 어셈블러만큼 느리고 느리기 때문에별로 유용하지 않았습니다. Norcroft는 멋진 최적화를 수행했지만 조건부 명령어 세트는 오늘날 컴파일러에게 문제가되었다고 생각합니다.
Jon Harrop

1
@AndrewM : 사실 ARM은 일종의 RISC입니다. 다른 RISC ISA는 컴파일러가 사용하는 것부터 시작하여 설계되었습니다. ARM ISA는 CPU가 제공하는 것부터 시작하여 설계된 것으로 보입니다 (배럴 시프터, 조건 플래그 → 모든 명령어에서이를 노출 시키자).
ninjalj

16

더 이상 적용되지 않고 괴상한 즐거움을위한 유스 케이스 : Amiga에서 CPU와 그래픽 / 오디오 칩은 특정 RAM 영역 (특정한 첫 번째 2MB RAM)에 액세스하기 위해 싸울 것입니다. 따라서 2MB 이하의 RAM 만 있으면 복잡한 그래픽과 사운드 재생이 CPU 성능을 저하시킵니다.

어셈블러에서는 그래픽 / 오디오 칩이 내부적으로 사용 중일 때 (예 : 버스가 비어있을 때) CPU가 RAM에 액세스하려고 시도 할 수있는 영리한 방식으로 코드를 인터리브 할 수 있습니다. 따라서 명령을 재정리하고, CPU 캐시를 효율적으로 사용하고, 버스 타이밍을 조정하면 모든 명령에 시간을 내야했기 때문에 더 높은 수준의 언어로는 불가능했던 몇 가지 효과를 얻을 수 있습니다. 서로 레이더에서 칩.

이것이 CPU의 NOP (No Operation-No do) 명령이 실제로 전체 응용 프로그램을 더 빠르게 실행할 수있는 또 다른 이유입니다.

[편집] 물론이 기술은 특정 하드웨어 설정에 따라 다릅니다. 많은 Amiga 게임이 더 빠른 CPU에 대처할 수 없었던 주된 이유는 다음과 같습니다.


Amiga에는 칩셋에 따라 512MB에서 2MB와 같은 16MB의 칩 RAM이 없었습니다. 또한 많은 Amiga 게임은 설명과 같은 기술로 인해 더 빠른 CPU에서 작동하지 않았습니다.
bk1e

1
@ bk1e-Amiga는 다양한 컴퓨터 모델을 생산했으며 Amiga 500은 512K 램이 1Meg로 확장되어 출하되었습니다. amigahistory.co.uk/amiedevsys.html은 128Meg 램과 아미입니다
데이비드 워터스

@ bk1e : 나는 정정되었습니다. 메모리가 고장날 수 있지만 첫 번째 24 비트 주소 공간 (예 : 16MB)으로 칩 RAM이 제한되지 않았습니까? 그리고 그 위에 Fast가 매핑 되었습니까?
Aaron Digulla

@Aaron Digulla는 : 위키 백과 칩 / 빠른 / 느린 RAM 사이의 차이에 대한 자세한 정보가 en.wikipedia.org/wiki/Amiga_Chip_RAM
bk1e

@ bk1e : 내 실수. 68k CPU에는 24 개의 주소 레인 만 있었기 때문에 16MB를 사용했습니다.
Aaron Digulla

15

답이 아닌 것을 지적하십시오.
프로그래밍하지 않아도 적어도 하나의 어셈블러 명령어 세트를 아는 것이 좋습니다. 이것은 프로그래머가 더 많은 것을 알고 더 나은 것을 추구하려는 끊임없는 탐구의 일부입니다. 또한 프레임 워크를 시작할 때 소스 코드가 없으며 적어도 무슨 일이 일어나고 있는지 대략 알지 못합니다. 또한 JavaByteCode와 .Net IL이 모두 어셈블러와 유사하므로 이해하는 데 도움이됩니다.

적은 양의 코드 또는 많은 시간이있을 때 질문에 대답합니다. 임베디드 칩에서 사용하기에 가장 유용합니다. 칩 복잡성이 낮고 이러한 칩을 대상으로하는 컴파일러의 경쟁이 치열하여 인간에게 유리한 균형을 이룰 수 있습니다. 또한 제한된 장치의 경우 종종 컴파일러에게 지시하기 어려운 방식으로 코드 크기 / 메모리 크기 / 성능을 교환합니다. 예를 들어이 사용자 작업이 자주 호출되지 않으므로 코드 크기가 작고 성능이 저하 될 수 있지만 비슷한 모양의이 다른 함수가 1 초마다 사용되므로 코드 크기가 더 크고 성능이 더 빠릅니다. 그것은 숙련 된 어셈블리 프로그래머가 사용할 수있는 일종의 트레이드 오프입니다.

또한 C 컴파일로 코딩하고 생성 된 어셈블리를 검사 한 다음 C 코드를 변경하거나 조정하고 어셈블리로 유지 관리 할 수있는 중간 영역이 많이 추가되고 싶습니다.

제 친구는 현재 소형 전기 모터를 제어하기위한 칩인 마이크로 컨트롤러를 연구하고 있습니다. 그는 저수준 c와 어셈블리의 조합으로 일합니다. 그는 한 번은 주 루프를 48 개 명령에서 43 개로 줄인 하루의 좋은 날에 대해 이야기했습니다. 또한 256k 칩을 채우기 위해 코드가 커지고 비즈니스가 새로운 기능을 원한다는 등의 선택에 직면 해 있습니다.

  1. 기존 기능 제거
  2. 기존 기능의 일부 또는 전부의 크기를 성능 저하로 줄일 수 있습니다.
  3. 더 높은 비용, 더 높은 전력 소비 및 더 큰 폼 팩터로 더 큰 칩으로 이동하는 것을지지하십시오.

필자는 어셈블리를 작성해야 할 필요성을 느낀 적이없는 포트폴리오 또는 언어, 플랫폼, 응용 프로그램 유형을 갖춘 상용 개발자로 추가하고 싶습니다. 나는 내가 얻은 지식에 대해 항상 감사하게 생각합니다. 그리고 때로는 그것에 디버깅했습니다.

나는 "어셈블러를 배워야하는 이유"라는 질문에 훨씬 더 답을 알았지 만, 그것이 더 빠를 때 더 중요한 질문이라고 생각합니다.

다시 한 번 시도해보십시오. 조립에 대해 생각해야합니다.

  • 저수준 운영 체제 기능 작업
  • 컴파일러 작업.
  • 매우 제한된 칩, 임베디드 시스템 등에서 작업

어셈블리를 생성 된 컴파일러와 비교하여 어느 것이 더 빠르거나 작거나 더 나은지 확인하십시오.

데이비드


4
소형 칩에 내장 된 애플리케이션을 고려해 +1 여기에 너무 많은 소프트웨어 엔지니어가 임베디드를 고려하지 않거나 스마트 폰 (32 비트, MB RAM, MB 플래시)을 의미한다고 생각합니다.
Martin

1
타임 임베디드 애플리케이션이 좋은 예입니다! 하드웨어에 대한 지식이 제한되어 있기 때문에 컴파일러가 익숙하지 않은 이상한 명령 (avr sbi과 같은 간단한 명령조차도 있음 cbi)이 종종 있습니다.
felixphew

15

아무도이 말을하지 않은 것에 놀랐습니다. strlen()기능 어셈블리로 작성하면 훨씬 빠릅니다! C에서 할 수있는 가장 좋은 일은

int c;
for(c = 0; str[c] != '\0'; c++) {}

조립 중에는 속도를 크게 높일 수 있습니다.

mov esi, offset string
mov edi, esi
xor ecx, ecx

lp:
mov ax, byte ptr [esi]
cmp al, cl
je  end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp

end_4:
inc esi

end_3:
inc esi

end_2:
inc esi

end_1:
inc esi

mov ecx, esi
sub ecx, edi

길이는 ecx입니다. 이것은 한 번에 4 개의 문자를 비교하므로 4 배 더 빠릅니다. 그리고 eax와 ebx의 고차 단어를 사용한다고 생각 하면 이전 C 루틴 보다 8 배 빠릅니다 !


3
이것은 strchr.nfshost.com/optimized_strlen_function에 있는 것과 어떻게 비교 됩니까 ?
ninjalj

@ninjalj : 그들은 같은 것입니다 :) 나는 그것이 C에서 이런 식으로 할 수 있다고 생각하지 않았습니다. 약간 향상 될 수 있습니다.
BlackBear

C 코드에서 각 비교 전에 여전히 비트 단위 AND 연산이 있습니다. 컴파일러가 높은 바이트와 낮은 바이트 비교로 줄일 수있을만큼 똑똑 할 수는 있지만 돈을 걸지 않을 것입니다. 실제로 (word & 0xFEFEFEFF) & (~word + 0x80808080)단어의 모든 바이트가 0이 아닌 경우 0 인 속성을 기반으로하는 더 빠른 루프 알고리즘이 있습니다.
user2310967

@MichaWiedenmann true, ax에서 두 문자를 비교 한 후 bx를로드해야합니다. 감사합니다
BlackBear

14

SIMD 명령어를 사용한 매트릭스 연산은 아마도 컴파일러 생성 코드보다 빠릅니다.


일부 컴파일러 (정확히 기억한다면 VectorC)는 SIMD 코드를 생성하므로 더 이상 어셈블리 코드 사용에 대한 인수가 아닙니다.
OregonGhost

컴파일러는 SSE 인식 코드를 작성하여 인수가 사실이 아님
vartec

5
이러한 상황 중 많은 경우 어셈블리 대신 SSE intrisics를 사용할 수 있습니다. 이렇게하면 코드를 더 이식 가능하게 만들 수 있습니다 (gcc visual c ++, 64 비트, 32 비트 등). 레지스터 할당을 할 필요가 없습니다.
Laserallan

1
물론 당신은 할 것이지만 C 대신 어셈블리를 사용해야하는 곳은 묻지 않았습니다 .C 컴파일러가 더 나은 코드를 생성하지 않을 때라고 말했습니다. 직접 SSE 호출 또는 인라인 어셈블리를 사용하지 않는 C 소스를 가정했습니다.
Mehrdad Afshari

9
그래도 Mehrdad가 옳다. SSE를 올바르게 구현하는 것은 컴파일러에게는 매우 어려운 일이며 심지어 대부분의 컴파일러가이를 사용하지 않는 명백한 상황 (사람의 경우)입니다.
Konrad Rudolph

13

몇 년 전부터 특정 예제를 제공 할 수는 없지만 손으로 ​​작성한 어셈블러가 컴파일러보다 성능이 우수한 경우가 많이있었습니다. 이유 :

  • 레지스터에서 인수를 전달하여 호출 규칙에서 벗어날 수 있습니다.

  • 레지스터 사용 방법을 신중하게 고려하고 메모리에 변수를 저장하지 않아도됩니다.

  • 점프 테이블과 같은 경우 인덱스를 경계 검사하지 않아도됩니다.

기본적으로 컴파일러는 최적화 작업을 매우 잘 수행하며 거의 항상 "충분히 충분"하지만, 매 사이클마다 크게 지불하는 일부 상황 (예 : 그래픽 렌더링)에서는 코드를 알고 있기 때문에 바로 가기를 수행 할 수 있습니다 컴파일러는 안전한면에 있어야하기 때문에 컴파일러에서 사용할 수 없었습니다.

사실, 나는 선 그리기 또는 다각형 채우기 루틴과 같은 루틴이 실제로 스택에서 작은 기계 코드 블록을 생성하고 실행하여 지속적인 의사 결정을 피하는 일부 그래픽 렌더링 코드에 대해 들었습니다. 선 스타일, 너비, 패턴 등

즉, 컴파일러가하고 싶은 것은 나를 위해 좋은 어셈블리 코드를 생성하지만 너무 영리하지는 않지만 대부분 그렇게합니다. 사실, 포트란에 대해 내가 싫어하는 것 중 하나는 코드를 "최적화"하려는 시도로 코드를 스크램블링하는 것입니다.

일반적으로 앱에 성능 문제가 발생하면 디자인이 낭비되기 때문입니다. 요즈음, 전체 앱이 수명의 1 인치 이내에 이미 조정되었지만 여전히 빠르지 않고 단단한 내부 루프에 모든 시간을 소비하지 않는 한 성능을 위해 어셈블러를 권장하지 않습니다.

추가 : 어셈블리 언어로 작성된 많은 앱을 보았으며 C, Pascal, Fortran 등과 같은 언어에 비해 주요 속도 이점은 프로그래머가 어셈블러로 코딩 할 때 훨씬 더 신중했기 때문입니다. 언어에 관계없이 하루에 약 100 줄의 코드를 작성하고 3 ~ 400 개의 명령어와 같은 컴파일러 언어로 작성합니다.


8
+1 : "전화 규칙에서 벗어날 수 있습니다." C / C ++ 컴파일러는 여러 값을 반환 할 때 빠는 경향이 있습니다. 그들은 종종 호출자 스택이 구조체에 연속적인 블록을 할당하고 호출자가 그것을 채우기 위해 그것에 대한 참조를 전달하는 sret 형식을 사용합니다. 레지스터에서 여러 값을 반환하는 것이 몇 배 더 빠릅니다.
Jon Harrop

1
@Jon : C / C ++ 컴파일러는 함수가 인라인 될 때 ​​올바르게 작동합니다 (인라인되지 않은 함수는 ABI를 준수해야합니다. 이는 C 및 C ++의 제한이 아니라 연결 모델)
Ben Voigt

@BenVoigt : 다음은 카운터 예제입니다. flyfrogblog.blogspot.co.uk/2012/04/…
Jon Harrop

2
함수 호출이 인라인되지 않습니다.
Ben Voigt

13

내 경험에서 몇 가지 예 :

  • 예를 들어, x86-64, IA-64, DEC Alpha 및 64 비트 MIPS 또는 PowerPC와 같은 많은 아키텍처는 64 비트 x 64 비트 곱셈을 지원하여 128 비트 결과를 생성합니다. GCC는 최근 이러한 지침에 대한 액세스를 제공하는 확장 기능을 추가했지만 해당 어셈블리가 필요했습니다. 그리고이 명령어에 액세스하면 RSA와 같은 것을 구현할 때 64 비트 CPU에서 큰 차이를 만들 수 있습니다. 때로는 성능이 4 배나 향상됩니다.

  • CPU 특정 플래그에 액세스합니다. 나를 많이 물린 것은 캐리 깃발입니다. 다중 정밀도 덧셈을 수행 할 때 CPU 캐리 비트에 액세스 할 수없는 경우 결과가 오버 플로우되었는지 확인하기 위해 대신 결과를 비교해야합니다. 더 나쁜 것은 데이터 액세스 측면에서 상당히 직렬화되어 최신 슈퍼 스칼라 프로세서의 성능을 저하시키는 것입니다. 수천 개의 정수를 연속으로 처리 할 때 addc를 사용할 수 있다는 것은 큰 승리입니다 (캐리 비트에 대한 경합에도 수퍼 스칼라 문제가 있지만 현대 CPU는 꽤 잘 처리합니다).

  • SIMD. 자동 벡터화 컴파일러조차도 비교적 간단한 경우 만 수행 할 수 있으므로 좋은 SIMD 성능을 원한다면 불행히도 코드를 직접 작성해야하는 경우가 종종 있습니다. 물론 어셈블리 대신 내장 함수를 사용할 수 있지만 일단 내장 레벨에 도달하면 기본적으로 컴파일러를 레지스터 할당 자 및 명 목적으로 명령 스케줄러로 사용하여 어셈블리를 작성합니다. (컴파일러가 함수 프롤로그를 생성 할 수 있기 때문에 SIMD에 내장 함수를 사용하는 경향이 있습니다. 그래서 함수 호출 규칙과 같은 ABI 문제를 처리하지 않고도 Linux, OS X 및 Windows에서 동일한 코드를 사용할 수 있습니다. SSE 본질은 실제로별로 좋지 않습니다. Altivec은 경험이 많지 않지만 더 좋아 보입니다).비트 슬라이스 AES 또는 SIMD 오류 수정 -알고리즘을 분석하고 그러한 코드를 생성 할 수있는 컴파일러를 상상할 수는 있지만 그러한 똑똑한 컴파일러는 기존 (최상의)에서 30 년 이상 떨어져 있다고 생각합니다.

한편, 멀티 코어 머신과 분산 시스템은 다른 방향으로 가장 큰 성능 향상을 가져 왔습니다. 어셈블리에서 내부 루프를 작성하는 데 20 %의 추가 속도를 제공하거나 여러 코어를 통해 실행하면 300 % 또는 머신 클러스터에서 실행 물론 높은 수준의 최적화 (선물, 메모 등)는 ML 또는 Scala와 같은 고급 언어에서 C 또는 asm보다 훨씬 쉽게 수행 할 수 있으며 종종 훨씬 더 큰 성능을 제공 할 수 있습니다. 따라서 항상 그렇듯이 트레이드 오프가 발생합니다.


2
@Dennis는 내가 쓴 이유입니다. '물론 어셈블리 대신 내장 함수를 사용할 수 있지만 일단 내장 레벨에 도달하면 기본적으로 컴파일러를 레지스터 할당 자 및 명 목적으로 명령 스케줄러로 사용하여 어셈블리를 작성합니다.'
Jack Lloyd

또한 내장 기반 SIMD 코드는 어셈블러로 작성된 동일한 코드 보다 읽기 어려운 경향이 있습니다 . 대부분의 SIMD 코드는 벡터에서 데이터의 암시 적 재 해석에 의존합니다. 이는 컴파일러 내장 함수가 제공하는 데이터 형식과 관련된 PITA입니다.
cmaster-monica reinstate

10

이미지는 수백만 개의 픽셀로 구성 될 수 있으므로 이미지를 재생할 때와 같이 긴밀하게 반복됩니다. 제한된 수의 프로세서 레지스터를 최대한 활용하는 방법을 파악하고 이해하면 차이가 생길 수 있습니다. 실제 샘플은 다음과 같습니다.

http://danbystrom.se/2008/12/22/optimizing-away-ii/

그런 다음 프로세서에는 컴파일러가 다루기에는 너무 특수한 난해한 명령어가 있지만 경우에 따라 어셈블러 프로그래머가이를 잘 활용할 수 있습니다. XLAT 명령을 예로 들어 보겠습니다. 정말 큰 루프에서 테이블 룩업을 수행해야하는 경우 테이블은 256 바이트로 제한됩니다!

업데이트 : 아, 일반적으로 루프에 대해 말할 때 가장 중요한 것이 무엇인지 생각해보십시오. 컴파일러는 흔히 반복되는 횟수에 대한 실마리가 없습니다! 프로그래머 만이 루프가 여러 번 반복 될 것이므로 추가 작업으로 루프를 준비하는 것이 좋거나 반복 횟수가 너무 짧아서 설정이 실제로 반복보다 더 오래 걸리는 경우가 있다는 것을 알고 있습니다 예상했다.


3
프로파일 지정 최적화는 루프 사용 빈도에 대한 컴파일러 정보를 컴파일러에 제공합니다.
Zan Lynx

10

생각보다 자주 C는 C 표준이 그렇게 말했기 때문에 어셈블리 코더의 관점에서 필요하지 않은 것처럼 보이는 일을해야합니다.

예를 들어 정수 승격 C에서 char 변수를 시프트하려면 일반적으로 코드가 실제로 단일 비트 시프트를 수행 할 것으로 기대합니다.

그러나 표준에서는 컴파일러가 시프트하기 전에 int로 부호 확장을 수행하고 결과를 char로 자른 다음 대상 프로세서의 아키텍처에 따라 코드를 복잡하게 할 수 있습니다.


소형 마이크로 용 고품질 컴파일러는 수년 동안 값의 상위 부분을 처리하지 않아도 결과에 의미있는 영향을 미치지 않을 수 있습니다. 승격 규칙은 문제를 야기하지만, 대부분의 경우 컴파일러가 어떤 코너 사례가 관련이 있는지와 관련이 없는지 알 수없는 경우에 발생합니다.
supercat

9

컴파일러가 생성하는 것의 디스 어셈블리를 보지 않으면 잘 작성된 C 코드가 실제로 빠르지 않은지 실제로 알 수 없습니다. 여러 번 당신은 그것을보고 "잘 작성된"주관적인 것을 봅니다.

따라서 가장 빠른 코드를 얻기 위해 어셈블러를 작성할 필요는 없지만, 같은 이유로 어셈블러를 알아야합니다.


2
"그래서 가장 빠른 코드를 얻기 위해 어셈블러로 작성할 필요는 없다"글쎄, 나는 컴파일러가 사소하지 않은 어떤 경우에도 최적의 일을하는 것을 보지 못했다. 숙련 된 사람은 거의 모든 경우에 컴파일러보다 더 잘 할 수 있습니다. 따라서 "가장 빠른 코드"를 얻으려면 어셈블러로 작성해야합니다.
cmaster-monica reinstate

@ cmaster 내 경험에 따르면 컴파일러 출력은 무작위입니다. 때때로 그것은 정말 좋고 최적이며 때로는 "이 쓰레기가 어떻게 배출 될 수 있 었는가"입니다.
sharptooth

9

나는 모든 해답을 (이상 30 이상) 읽고 간단한 이유를 찾을 수 없습니다 : 어셈블러 빠른 C보다 당신이 읽고 실행 한 경우 인텔 ® 64 및 IA-32 아키텍처 최적화 참조 설명서 , 이유되는 것이므로 조립 할 수있다 더 느리다는 것은 이러한 느린 어셈블리를 작성하는 사람들이 Optimization Manual을 읽지 않았기 때문 입니다.

인텔 80286의 옛날에는 각 명령이 고정 된 수의 CPU 주기로 실행되었지만 1995 년 Pentium Pro가 출시 된 이후 인텔 프로세서는 복잡한 파이프 라이닝 : 주문 외 실행 및 레지스터 이름 변경을 사용하여 슈퍼 스칼라가되었습니다. 그 이전에는 1993 년에 생산 된 Pentium에는 U 및 V 파이프 라인이있었습니다. 이중 파이프 라인은 서로 의존하지 않으면 한 클록 사이클에서 두 개의 간단한 명령을 실행할 수 있습니다. 그러나 이것은 Pentium Pro에 등장한 주문 중 실행 및 등록 이름 변경과 비교할만한 것이 아니며 요즘 거의 변경되지 않았습니다.

몇 마디로 설명하자면, 가장 빠른 코드는 명령어가 이전 결과에 의존하지 않는 곳입니다. 예를 들어 항상 movzx로 전체 레지스터를 지우거나 add rax, 1대신 또는inc rax 이전 플래그 상태에 대한 종속성을 제거해야합니다.

시간이 허락하면 인터넷에서 사용할 수있는 많은 정보가있는 경우 주문 실패 실행 및 등록 이름 변경에 대해 자세히 읽을 수 있습니다.

분기 예측,로드 및 저장 장치 수, 마이크로-옵스를 실행하는 게이트 수 등과 같은 다른 중요한 문제도 있지만 가장 중요하게 고려해야 할 사항은 Out-of-Order Execution입니다.

대부분의 사람들은 단순히 주문 실패 실행에 대해 알지 못하므로 80286과 같은 어셈블리 프로그램을 작성하므로 상황에 관계없이 명령을 실행하는 데 고정 시간이 걸릴 것으로 예상합니다. C 컴파일러는 Out-of-Order Execution을 인식하고 코드를 올바르게 생성합니다. 그렇기 때문에 그러한 인식하지 못하는 사람들의 코드는 느리지 만 인식하게되면 코드가 더 빨라집니다.


8

어셈블러가 더 빠를 때의 일반적인 경우는 스마트 어셈블리 프로그래머가 컴파일러의 출력을보고 "이것이 성능의 중요한 경로이며 더 효율적으로 쓸 수 있습니다"라고 말한 다음 그 사람이 어셈블러를 수정하거나 다시 작성하는 것입니다. 기스로부터.


7

그것은 모두 당신의 작업량에 달려 있습니다.

일상적인 작업의 경우 C 및 C ++은 훌륭하지만 어셈블리를 수행해야하는 작업이 많이 필요한 특정 작업 (비디오 (압축, 압축 해제, 이미지 효과 등) 변환)이 있습니다.

또한 일반적으로 이러한 종류의 작업에 맞게 조정 된 CPU 특정 칩셋 확장 (MME / MMX / SSE / 무엇이든)을 사용합니다.


6

인터럽트 당 192 또는 256 비트에서 50 마이크로 초마다 발생하는 비트 전치 작업이 있습니다.

고정 맵 (하드웨어 제약 조건)에 의해 발생합니다. C를 사용하면 약 10 마이크로 초가 걸렸습니다. 이 맵의 특정 기능, 특정 레지스터 캐싱 및 비트 지향 연산을 고려하여 이것을 어셈블러로 변환 할 때; 수행하는 데 3.5 마이크로 초 미만이 소요되었습니다.


6

Walter Bright의 불변 및 순도 최적화를 살펴볼 가치가 있습니다 . 프로파일 테스트는 아니지만 필기 및 컴파일러 생성 ASM의 차이점에 대한 좋은 예를 보여줍니다. Walter Bright는 최적화 컴파일러를 작성하여 다른 블로그 게시물을 살펴볼 가치가 있습니다.



5

간단한 대답 ... 어셈블리를 아는 사람 (일명 그 옆에 참조가 있으며 모든 작은 프로세서 캐시 및 파이프 라인 기능 등을 활용 함)은 모든 컴파일러 보다 훨씬 빠른 코드를 생성 할 수 있습니다 .

그러나 요즘의 차이점은 일반적인 응용 프로그램에서는 중요하지 않습니다.


1
당신은 "많은 시간과 노력을 기울이고", "유지 보수 악몽 만들기"라고 말하는 것을 잊었습니다. 내 동료가 OS 코드의 성능에 중요한 부분을 최적화하기 위해 노력하고 있었으며 C에서 일한 것이 아니라 합리적인 시간 내에 높은 수준의 변경으로 인한 성능 영향을 조사 할 수있었습니다.
Artelius

동의한다. 때로는 시간을 절약하고 빠르게 개발하기 위해 매크로와 스크립트를 사용하여 어셈블리 코드를 생성합니다. 요즘 대부분의 어셈블러에는 매크로가 있습니다. 그렇지 않은 경우 (정말 간단한 RegEx) Perl 스크립트를 사용하여 간단한 매크로 전처리기를 만들 수 있습니다.

이. 정확하게. 도메인 전문가를 이길 컴파일러는 아직 발명되지 않았습니다.
cmaster-monica reinstate

4

CP / M-86 버전의 PolyPascal (Turbo Pascal과 동급)에 대한 가능성 중 하나는 "생체에서 출력 문자로 화면 사용"기능을 기계 언어 루틴으로 대체하는 것이 었습니다. x, y, 그리고 거기에 넣을 문자열이 주어졌습니다.

이를 통해 이전보다 훨씬 빠르게 화면을 업데이트 할 수있었습니다!

바이너리에는 머신 코드 (수백 바이트)를 넣을 공간이 있었고 다른 것들도 있었으므로 가능한 한 많이 짜야했습니다.

화면이 80x25이기 때문에 두 좌표가 각각 1 바이트에 맞을 수 있으므로 두 바이트 모두 2 바이트 단어에 맞을 수 있습니다. 이를 통해 단일 추가로 두 값을 동시에 조작 할 수 있으므로 더 적은 바이트로 필요한 계산을 수행 할 수있었습니다.

내 지식으로는 레지스터에 여러 값을 병합하고 SIMD 명령을 수행하고 나중에 다시 분할 할 수있는 C 컴파일러가 없습니다 (기계 명령이 더 짧을 것이라고 생각하지 않습니다).


4

가장 유명한 어셈블리 스 니펫 중 하나는 Michael Abrash의 텍스처 매핑 루프 ( 자세한 내용은 여기 참조 ) 에서 가져온 것입니다 .

add edx,[DeltaVFrac] ; add in dVFrac
sbb ebp,ebp ; store carry
mov [edi],al ; write pixel n
mov al,[esi] ; fetch pixel n+1
add ecx,ebx ; add in dUFrac
adc esi,[4*ebp + UVStepVCarry]; add in steps

오늘날 대부분의 컴파일러는 고급 CPU 특정 명령어를 내장 명령어, 즉 실제 명령어로 컴파일되는 함수로 표현합니다. MS Visual C ++는 MMX, SSE, SSE2, SSE3 및 SSE4에 대한 내장 함수를 지원하므로 플랫폼 별 명령어를 활용하기 위해 어셈블리로 드롭 다운하는 것에 대해 걱정할 필요가 없습니다. Visual C ++는 적절한 / ARCH 설정으로 대상으로하는 실제 아키텍처를 활용할 수도 있습니다.


더 좋은 점은 이러한 SSE 내장 함수가 인텔에서 지정했기 때문에 실제로 이식성이 뛰어납니다.
James

4

올바른 프로그래머가 주어지면 어셈블러 프로그램은 항상 C 프로그램보다 빠릅니다 (적어도 약간). 적어도 하나의 어셈블러 명령을 수행 할 수없는 C 프로그램을 작성하는 것은 어렵습니다.


이것은 좀 더 정확한 것입니다 : "만들 어려울 것이다 사소 여기서 C 프로그램을 ..."또는, 당신은 말할 수있다 : "어려울 것이다 실제 찾을 포인트입니다 ... C 프로그램을" 컴파일러가 최적의 출력을 생성하는 간단한 루프가 있습니다. 그럼에도 불구하고 좋은 대답입니다.
cmaster-monica reinstate


4

gcc는 널리 사용되는 컴파일러가되었습니다. 일반적으로 최적화는 그리 좋지 않습니다. 일반적인 프로그래머 쓰기 어셈블러보다 훨씬 좋지만 실제 성능에는 좋지 않습니다. 그들이 생성하는 코드에서 단순히 놀라운 컴파일러가 있습니다. 따라서 일반적인 대답으로 컴파일러의 출력으로 이동하여 어셈블러를 조정하여 성능을 조정하거나 루틴을 처음부터 다시 작성할 수있는 곳이 많이 있습니다.


8
GCC는 매우 지능적인 "플랫폼 독립적"최적화를 수행합니다. 그러나 특정 명령어 세트를 최대한 활용하는 것은 그리 좋지 않습니다. 이러한 휴대용 컴파일러의 경우 매우 잘 작동합니다.
Artelius 2016 년

2
동의했다. 휴대 성과 언어, 대상이 훌륭합니다. 휴대가 가능하다는 것은 한 언어 나 목표를 능숙하게 할 수있는 방법이 될 수 있습니다. 따라서 인간이 더 잘할 수있는 기회는 특정 목표에 대한 특정 최적화를위한 것입니다.
old_timer 2016 년

+1 : GCC는 확실히 빠른 코드를 생성하는 데있어 경쟁력이 없지만 이식성이 뛰어 나기 때문에 확실하지 않습니다. LLVM은 이식성이 뛰어나 GCC보다 4 배 빠른 코드를 생성하는 것을 보았습니다.
Jon Harrop

GCC는 수년 동안 견고 해 졌으므로 현대 휴대용 컴파일러를 실행할 수있는 거의 모든 플랫폼에서 사용할 수 있기 때문에 GCC를 선호합니다. 불행히도 나는 LLVM (Mac OS X / PPC)을 만들 수 없었기 때문에 아마 LLVM으로 바꿀 수 없을 것입니다. GCC의 장점 중 하나는 GCC로 빌드되는 코드를 작성하면 표준에 가깝게 유지되며 거의 모든 플랫폼에 대해 빌드 될 수 있다는 것입니다.

4

롱 포크에는 시간이라는 제한이 있습니다. 코드에 대한 모든 단일 변경을 최적화하고 레지스터 할당에 시간을 소비하고 유출을 최소화하고 그렇지 않은 것을 최적화 할 수있는 리소스가 없을 때 컴파일러는 매번 승리합니다. 코드를 수정하고 다시 컴파일하고 측정합니다. 필요한 경우 반복하십시오.

또한 높은 수준에서 많은 것을 할 수 있습니다. 또한 결과 어셈블리를 검사하면 코드가 손상되었다는 인상을 줄 수 있지만 실제로는 생각보다 빠를 것입니다. 예:

int y = 데이터 [i]; // 여기서 몇 가지 작업을 수행하십시오. call_function (y, ...);

컴파일러는 데이터를 읽고 스택으로 옮긴 다음 (스필) 나중에 스택에서 읽고 인수로 전달합니다. 시끄러운 소리? 실제로 매우 효과적인 대기 시간 보상이 될 수 있으며 런타임이 빨라집니다.

// 최적화 된 버전 call_function (data [i], ...); // 결국에는 최적화되지 않았습니다.

최적화 된 버전의 아이디어는 레지스터 압력을 줄이고 유출을 피한다는 것입니다. 그러나 사실 "똥"버전이 더 빨랐습니다!

어셈블리 코드를 살펴보면 지침을보고 결론을 내릴 수 있습니다. 명령이 많을수록 속도가 느리면 잘못된 판단입니다.

임금의 관심에 여기 것입니다 : 많은 조립 전문가들은 생각 들이 많이 알고 있지만, 거의 알고있다. 규칙도 아키텍처에서 다음으로 변경됩니다. 예를 들어, 항상 가장 빠른 은색 불렛 x86 코드는 없습니다. 요즘 규칙을 따르는 것이 좋습니다.

  • 기억이 느리다
  • 캐시가 빠릅니다
  • 더 나은 캐시를 사용하려고
  • 얼마나 자주 그리워할까요? 지연 시간 보상 전략이 있습니까?
  • 단일 캐시 미스에 대해 10-100 ALU / FPU / SSE 명령어를 실행할 수 있습니다
  • 응용 프로그램 아키텍처가 중요합니다.
  • .. 그러나 문제가 아키텍처에없는 경우에는 도움이되지 않습니다.

또한, 생각조차하지 못한 C / C ++ 코드를 "이론적으로 최적 인"코드로 마술처럼 변형시키는 컴파일러를 지나치게 신뢰하는 것은 희망적인 생각입니다. 이 저수준에서 "성능"에 관심이 있다면 사용하는 컴파일러와 툴 체인을 알아야합니다.

C / C ++의 컴파일러는 일반적으로 함수에 부작용이 있기 때문에 하위 표현식의 순서를 다시 정렬하는 데별로 좋지 않습니다. 기능적 언어는이 경고로 고통받지 않지만 현재의 생태계에 잘 맞지 않습니다. 컴파일러 / 링커 / 코드 생성기에 의해 연산 순서를 변경할 수있는 완화 된 정밀 규칙을 허용하는 컴파일러 옵션이 있습니다.

이 주제는 약간의 막 다른 골목입니다. 대부분 관련이 없으며 나머지는 어쨌든 이미하고있는 일을 알고 있습니다.

그것은 "당신이 무엇을하고 있는지 이해하기 위해서"로 요약됩니다. 그것은 당신이 무엇을하고 있는지 아는 것과는 조금 다릅니다.

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