1P by neo 7달전 | favorite | 댓글 1개

코드 속도 향상: AMD64에서 16바이트보다 큰 구조체를 전달하지 마세요

  • Neat 언어의 성능 향상을 위해 배열을 하나의 구조체 파라미터 대신 세 개의 포인터 파라미터로 전달하는 방식으로 변경함.
  • Neat 배열이 D 언어의 배열보다 느렸던 이유는 24바이트 크기의 배열이 16바이트를 초과하여 다른 방식으로 파라미터를 전달하기 때문임.
  • SystemV AMD64 ABI 명세에 따르면, 16바이트를 초과하는 모든 구조체는 포인터를 통해 전달됨.

벤치마크를 통한 문제 확인

  • 벤치마크를 통해 구조체를 전달하는 방식과 개별 필드를 전달하는 방식의 성능 차이를 확인함.
  • 구조체를 전달할 때는 스택에 할당하고 복사하는 과정이 필요하지만, 개별 필드를 전달할 때는 SSE 레지스터를 통해 바로 전달됨.
  • 개별 필드를 전달하는 방식은 구조체를 전달하는 방식에 비해 약 2배 빠른 성능을 보임.

언어 설계자의 선택

  • C API를 호출할 때는 C ABI를 따라야 하지만, 내부적으로 사용되는 고급 타입은 구조체로 표현될 필요가 없음.
  • 언어 설계자는 배열, 튜플, 합 타입 등이 어떻게 전달될지 결정할 수 있음.
  • 16바이트를 초과하는 타입을 개별 필드로 전달하는 것이 성능 향상에 도움이 될 수 있음.

GN⁺의 의견

  • 이 기사는 소프트웨어 최적화에 관심이 있는 개발자들에게 매우 유익함.
  • 특히, 성능에 민감한 애플리케이션을 개발할 때 구조체의 크기와 전달 방식이 중요한 영향을 미칠 수 있음을 보여줌.
  • 언어 설계자나 API 개발자는 이 정보를 활용하여 성능을 개선할 수 있는 기회를 얻을 수 있음.
Hacker News 의견
  • SysV amd64 ABI 문제와 관련하여, 언어 내부 ABI를 SysV가 아닌 다른 것으로 설정할 수 있음. SysV C 호출자에게 노출되지 않는 한, 원하는 호출 규칙을 사용할 수 있음. NeatLang의 차이점은 LLVM 호출 규칙을 변경하는 것보다 훨씬 복잡해 보이며, 저자는 C 프로그램에 일정한 호출 규칙으로 타입을 노출하고자 할 수도 있음.
  • 인수 전달 비용에 대한 이해가 부족한 경우가 많으며, 이에 대해 작성된 글이 유익함. 예를 들어 Google에서는 24바이트 객체를 값으로 전달하는 관행이 프로파일러에 나타나지 않지만 모든 함수에서 비용이 발생함.
  • x64로 전환할 때, vec3 객체(3xfloat)가 12바이트에서 16바이트로 확장되는 것에 대해 우려하여 그래픽 엔진을 벤치마킹함. 16바이트를 사용하는 것이 8바이트 읽기에 맞춰져 있어 더 빠르다는 것을 발견함. 결과적으로 vec3는 vec4처럼 사용됨. 항상 전체적인 벤치마킹을 수행할 것을 권장함.
  • 레지스터에 미리 로드된 인수가 스택 쓰기보다 성능이 뛰어나며, 스택 조작은 힙 할당된 것보다 더 빠름. 이는 전역 변수가 많은 복잡한 코드가 빠르게 실행되고, 우아한 재귀 함수나 튜플/구조체/리스트 인수가 느린 이유임. 전자는 조밀한 어셈블리 루프로 최적화하기 쉬움.
  • MSVC에서는 8바이트를 초과하는 구조체가 스택에 전달됨. 이는 이식 가능한 코드에서 의존해선 안 될 ABI 세부 사항임. 그러나 자주 호출되지 않는 함수의 경우에는 너무 스트레스 받지 말고, 자주 호출되는 작은 함수의 경우에는 컴파일러가 코드를 인라인할 수 있도록 하여 레지스터에서 인수를 전달하는 것 이상의 유용한 최적화를 활성화함.
  • Windows에서 기본 cdecl 호출 규칙을 사용할 때 8바이트보다 큰 구조체는 레지스터에 전달되지 않음.
  • amd64에서 sysv amd64 ABI를 사용하여 16바이트를 초과하는 구조체를 값으로 전달하고 반환하는 것은 느리지만 코드를 명확하게 만드는 데 종종 가치가 있음. 물론 이 경우에는 해당되지 않지만, 예를 들어 각 C++ 컴파일러, Golang, OCaml, SBCL과 같이 자체 언어 내에서 사용자 정의 ABI를 사용할 수 있음.
  • C++에서는 비원시 타입은 좋은 이유가 없는 한 참조(또는 필요하다면 포인터)로 전달해야 한다는 경험칙이 있음. 이는 ABI 때문이기도 하고 복사 또는 이동 생성자를 피하기 위함임. 성능 최적화를 원한다면 C++에서 주의를 기울여야 할 지루한 저수준 세부 사항임.
  • 기사는 매우 특수한 벤치마크에 대한 링크를 제공하며, 여기서는 Java(JIT)가 C++보다 빠르고 심지어 Scala보다도 빠름. Julia HO가 무엇이며 왜 그렇게 빠른지, Python과 Pypy 사이의 속도 차이가 크며 Pypy를 사용하지 않을 이유가 있는지, 표준이 되어야 하는지에 대한 의문을 제기함.
  • 제공된 예에서는 호출자에게 영향을 주지 않고 “struct Vector” 매개변수 유형을 “const struct Vector &” 참조로 전달하도록 변경하여 수정할 수 있음. 포인터 버그가 존재하는 많은 C++ 코드가 포인터를 불필요하게 사용했으며, 참조로 전달하는 것이 더 쉽고 안전하게 사용할 수 있었음.