3P by GN⁺ | ★ favorite | 댓글 1개
  • CORDIC은 FPU나 큰 조회 테이블 없이 sin, cos, tan 같은 삼각함수를 계산하기 위해, 복잡한 연산을 덧셈과 비트 시프트 중심으로 바꾸는 알고리듬임
  • 이 방식은 고성능 시스템보다 임베디드 환경, 특히 성능이 낮은 마이크로컨트롤러와 FPGA에서 유용하며, 속도만으로 가치를 판단하기 어려움
  • 부동소수점 대신 고정소수점을 쓰면 int32_t의 상위 16비트를 정수부, 하위 16비트를 소수부로 나눠 약 -32768.99997부터 32767.99997까지 표현할 수 있음
  • 벡터를 목표 각도에 맞춰 점점 작은 각도로 회전시키고, atan(2**-i) 테이블 16개와 x=39796 초기값을 쓰면 반복마다 곱셈을비트 시프트로 대체할 수 있음
  • 예시 각도 0.9152를 16회 반복하면 sin(0.9152) 절대 오차는 0.00000956, cos(0.9152) 절대 오차는 0.0000434 수준까지 줄어듦

CORDIC이 잘 맞는 계산 환경

  • CORDICsin, cos, tan 같은 삼각함수를 저전력 하드웨어에서 계산하기 위한 알고리듬임
  • FPU, 즉 부동소수점 연산 장치가 없거나 큰 조회 테이블을 쓰기 어려운 환경에서도 동작함
  • 실제 연산은 단순한 덧셈비트 시프트 중심으로 구성됨
  • 벡터 수학, 삼각법, 수렴, 컴퓨터 과학적 아이디어를 결합해 복잡한 함수를 단순 연산으로 근사함
  • 고성능 하드웨어에서는 이 기법이 꼭 필요하지 않을 수 있음
    • 주요 적용 대상은 임베디드 환경임
    • 특히 성능이 낮은 마이크로컨트롤러와 FPGA에 적합함
    • 더 빠른 하드웨어나 주변장치가 있을 수도 있지만, 속도만이 유용성의 유일한 척도는 아님

부동소수점을 피하는 고정소수점 표현

  • sin(x)처럼 -1.0에서 1.0 사이 값을 내는 함수도 반드시 부동소수점으로 표현할 필요는 없음
  • 고정소수점은 정수 타입 안에서 소수점 위치를 고정해 유리수를 나타냄
  • 예시는 int32_t를 상위 16비트 정수부, 하위 16비트 소수부로 나눔
    • 이 경우 범위는 대략 -32768.99997부터 32767.99997까지임
    • 소수점 위치를 어디에 두느냐에 따라 정수부 범위와 소수부 정밀도를 맞바꿀 수 있음
  • 값 자체는 여전히 int32_t이며, 프로그래머가 비트 배열에 추가 의미를 부여함

고정소수점 변환과 기본 연산

  • 소수 정밀도가 16비트라면 42.01 같은 float 값에 (1 << 16)을 곱해 고정소수점 값으로 만들 수 있음
    • 42.01 * (1 << 16)int32_t로 캐스팅하면 2753167이 됨
    • 다시 float로 바꾸려면 2753167 / (1 << 16)을 계산해 약 42.0099945를 얻음
  • 부동소수점을 전혀 쓰지 않고 1.5 같은 값을 직접 인코딩할 수도 있음
    • 정수부 1(1 << 16)으로 올림
    • 소수부 절반은 0x00000xffff 사이의 중간값인 0x7fff로 둘 수 있음
    • 이 방식의 결과는 십진수 98303
  • 같은 스케일링 계수를 쓰는 값끼리는 덧셈뺄셈이 그대로 동작함
  • 곱셈은 두 고정소수점 값을 곱한 뒤 결과를 스케일링 계수만큼 다시 오른쪽으로 시프트함
  • 나눗셈은 피제수를 스케일링 계수만큼 미리 왼쪽으로 시프트한 뒤 제수로 나누면 추가 정밀도를 얻을 수 있음

벡터 회전으로 삼각함수 근사하기

  • CORDIC은 “co-ordinate rotation digital computer”의 약자이며, 1950년대 중반에 만들어짐
  • 핵심 아이디어는 단위원 위에서 벡터를 점점 작은 각도로 회전시켜, 목표 각도에 도달했을 때 벡터 성분이 사인과 코사인 값이 되게 하는 것임
  • 이 과정은 이진 탐색과 비슷하게 진행됨
    • 목표 각도를 향해 큰 각도로 이동함
    • 목표를 지나쳤는지 확인함
    • 이후 더 작은 각도로 시계 방향 또는 반시계 방향 회전을 반복함
  • 예시로 sin(0.7)을 구할 때는 초기 벡터 (1, 0)와 목표 0.7 라디안에서 시작함
    • 먼저 0.7853 라디안, 즉 45˚를 반시계 방향으로 회전함
    • 남은 목표는 0.7 - 0.7853 = -0.0853이 됨
    • 값이 음수이므로 다음에는 0.3926 라디안, 즉 22.5˚를 시계 방향으로 회전함
    • 이후 남은 목표의 부호에 따라 0.1963 라디안 등 더 작은 각도로 방향을 바꿔가며 회전함
  • 16회 반복하면 벡터가 원래 목표 각도에 거의 맞춰지고, ysin(a), xcos(a)의 근사가 됨

회전 행렬에서 비싼 연산 줄이기

  • 일반적인 벡터 회전은 사인과 코사인이 들어간 행렬 곱셈을 사용함
  • CORDIC은 삼각 항등식을 이용해 회전 행렬을 tan(a) 중심으로 바꿈
  • 처음에는 45˚, 22.5˚, 11.25˚처럼 정해진 회전 각도를 쓰므로 tan(a) 값을 미리 계산한 테이블로 둘 수 있음
  • 이 테이블은 16개의 uint32_t만 필요해 64바이트임
    • 비교 대상으로, -1부터 1까지 4096개 값을 담는 최적화되지 않은 sin(x) 테이블은 16KiB가 필요하고 정밀도도 낮다고 봄
  • 회전마다 앞에 붙는 cos(a) 항은 반복마다 생기지만, 모두 곱한 값은 상수로 수렴함
    • 45˚, 22.5˚, 11.25˚ 식의 각도를 쓸 때 이 곱은 약 0.6366
    • 이 상수를 모든 반복 뒤에 한 번 곱하면 됨

시프트와 덧셈만 남기는 각도 선택

  • 곱셈을 없애려면 tan(a) 결과가 항상 2의 역거듭제곱이 되도록 각도를 고름
  • 이를 위해 각 반복 i=0부터 15까지 atan(2**-i) 값을 담은 16개 엔트리 테이블을 만듦
  • 실제 회전 각도는 45˚, 26.565˚, 14.036˚, 7.125˚ 등이 됨
  • 각도가 정확히 절반씩 줄어들지는 않지만, 이 각도들을 사용해도 과정은 올바른 결과로 수렴함
  • tan(a) 곱셈은 반복 번호 i만큼의 비트 시프트로 바뀜
  • cos(a) 항들의 곱도 새 각도 선택에 맞춰 다시 계산됨
    • 값은 약 0.60725
    • 16비트 고정소수점으로는 39796이 됨
    • 마지막에 곱하는 대신 초기 벡터의 x1이 아니라 39796으로 설정하면 됨

알고리듬 절차

  • 사전 계산 단계에서는 각 엔트리가 atan(2**-i)인 테이블을 만들고, 각 값을 고정소수점으로 변환함
    • 변환식은 atan(2**-i) * (1 << 16)
  • sin이나 cos를 구할 때는 입력 각도도 고정소수점으로 바꿈
    • 예시 0.91520.9152 * (1 << 16) = 59978이 됨
  • 초기 상태는 다음과 같음
x = 39796
y = 0
z = 59978
  • z는 벡터의 일부가 아니라 남은 목표 각도를 추적하는 값임
  • z의 부호가 회전 방향을 결정함
    • z >= 0이면 반시계 방향으로 회전하고 z -= table[i]를 수행함
    • z < 0이면 시계 방향으로 회전하고 z += table[i]를 수행함
  • 각 반복은 xy에 대해 덧셈, 뺄셈, >> i 시프트만 사용함
if z >= 0:
    x_next = x - (y >> i)
    y_next = y + (x >> i)
    z -= table[i]
else:
    x_next = x + (y >> i)
    y_next = y - (x >> i)
    z += table[i]
x = x_next
y = y_next

예시 수렴 결과와 남은 주제

  • 0.9152 라디안 예시에서는 첫 반복에서 z가 양수이므로 약 0.785 라디안만큼 반시계 방향으로 회전함
  • 두 번째 반복에서도 z가 양수여서 약 0.436 라디안 반시계 방향으로 회전하지만 목표를 지나침
  • 세 번째 반복에서는 z가 음수가 되어 약 0.244 라디안만큼 시계 방향으로 회전함
  • 네 번째 반복도 z가 음수여서 약 0.124 라디안 시계 방향으로 회전함
  • 각도 변화가 작아질수록 벡터는 실제 결과 가까이에서 앞뒤로 움직이며 수렴함
  • 16회 반복 뒤 ysin(0.9152)의 매우 가까운 근사가 됨
    • 사인 절대 오차는 0.00000956
    • x의 코사인 절대 오차는 0.0000434
  • 다루지 않은 주제도 남아 있음
    • 관심 각도가 단위원의 1사분면이나 4사분면 밖에 있을 때 필요한 특별 처리
    • CORDIC 변형으로 계산 가능한 tan, atan, asin, acos, sinh, cosh, tanh, sqrt, ln, e^x
    • 로그와 지수 계산을 위해 설계된 관련 알고리듬 BKM
  • 관련 내용을 Low Byte Productions YouTube channel에서 더 자세히 다룰 계획이 있음

댓글과 토론

Hacker News 의견들
  • 저자는 주로 FPGA 같은 곳에 적용된다고 했지만, 게임 개발이나 분산 물리 시뮬레이션에도 쓸 수 있음
    부동소수점 계산은 플랫폼 간 결정성을 맞추기 까다롭고, 한 가지 해법은 부동소수점을 아예 피해서 고정소수점 물리 엔진을 구현하는 것임
    삼각함수 기능을 구현하려면 CORDIC 같은 것이 필요함
    몇 년 전 재미 삼아 이런 걸 만들기 시작했지만 끝내지는 못했고, 언젠가 다시 해보고 싶음
    https://randomascii.wordpress.com/2013/07/16/floating-point-...

    • 그 글은 이제 10년이 되었지만 중요한 인용이 들어 있음: “IEEE 표준은 몇 가지를 보장한다. 부동소수점 수학을 신비주의처럼 보는 사람들이 아는 것보다는 많이 보장하지만, 일부 프로그래머가 생각하는 것보다는 적게 보장한다”
      요약하면 x87은 이상한 점이 있었고, 반올림 모드와 0으로 플러시 같은 설정을 일관되게 맞춰야 하며, 오래된 프로세서에는 FMA가 없고, mmsqrtps 같은 근사 명령은 일관된 명세가 없으며, 컴파일러가 식을 재결합할 수 있음
      작은 루틴이나 직접 작성한 라이브러리라면 고통스럽긴 해도 이런 것들을 피하도록 보장하는 게 가능함
      IEEE-754 2008은 명세를 더 명확히 했고 사실상 x87의 죽음을 전제로 했으며, 2024년에는 x87을 확실히 피할 수 있음
      FMA도 IEEE-754 2008 명세의 일부이고 Intel Haswell 이후를 포함해 현대 프로세서에는 들어가 있음
      그래도 8-wide AVX2와 4-wide NEON 같은 아키텍처 차이가 발목을 잡을 수 있지만, 어셈블리나 인트린식, 혹은 Compiler Explorer나 objdump로 확인하는 C를 쓰면 출력물을 보고 “이건 일관되겠다”고 판단할 수 있음
    • 저자도 부동소수점이 하드웨어 성능 향상으로 널리 쓰이기 전에는 고정소수점이 게임 개발에서 매우 흔했고, CORDIC도 함께 쓰였을 가능성이 높다고 했음
      “사실 IEEE 754가 오늘날처럼 대중적인 표준이 되기 전에는 고정소수점이 항상 쓰였다. 1980년부터 2000년쯤 사이에 작업한 게임 개발자에게 물어보면 자세히 말해줄 것이다”
    • 게임 개발용 물리 시뮬레이션 라이브러리 nphysics는 플랫폼 간 결정성이 필요할 때 CORDIC와 함께 고정소수점 수학을 쓰는 방식을 택했지만, 지금은 폐기됨
      nphysics를 다시 쓴 새 라이브러리 Rapier는 대신 IEEE-754 2008의 보장에 기대어 플랫폼 간 결정성을 제공함
      그래서 오래된 플랫폼에서는 동작하지 않지만, wasm을 포함한 현대 플랫폼에서는 결정적임
      물론 각 플랫폼이 제공하는 sin, cos 같은 초월함수 루틴에 의존할 수는 없고, 어디서나 같은 방식으로 동작하도록 직접 구현해야 함
      하지만 비준수 플랫폼에서 실행하지 않으면 가능한 접근임
      https://www.rustsim.org/blog/2020/06/01/this-month-in-rustsi...
      https://rapier.rs/docs/user_guides/rust/determinism/
  • CORDIC는 사인과 코사인 계산·생성뿐 아니라 로그, 지수, 제곱근, 벡터 크기, 극좌표-직교좌표 변환, 벡터 회전 같은 여러 연산에도 사용할 수 있음
    저자도 결론에서 이런 가능성을 예고함
    기존의 직교정규 행렬 대신 쿼터니언을 쓰면 CORDIC 기반 연산을 더 효율적으로, 즉 계산 사이클과 메모리를 덜 쓰고 오차도 줄이며 실행할 수 있을 것 같은 느낌이 듦
    https://core.ac.uk/works/8439118

    • 기억이 맞다면 임의의 리 군으로도 확장할 수 있음
  • 고등학교 예비미적분에서 테일러 급수를 배웠고, 선생님이 계산기의 삼각함수가 실제로 그렇게 구현된다고 말했음
    찾아보니 실제로는 CORDIC였고, TI Basic으로 구현해보며 재미있게 놀았음

    • 놀라운 Sinclair scientific 계산기가 삼각함수와 로그 등을 어떻게 계산했는지 읽어보면 흥미로울 것 같음
      CORDIC는 아니었지만 알고리즘에 비슷한 점이 있음
      http://files.righto.com/calculator/sinclair_scientific_simul...
    • 테일러 전개를 쓰는 계산기가 실제로 하나라도 있나?
  • 하드웨어 구현 관련 글들:
    https://arxiv.org/pdf/2211.04053
    https://hal.science/hal-01327460/document
    https://archive.ll.mit.edu/HPEC/agendas/proc05/Day_1/Abstrac...
    시대별 다양한 하드웨어에서 일반적인 소프트웨어·하드웨어 삼각함수 구현과 어떻게 비교되는지 보고 싶음

  • sin과 cos는 벡터 회전에 자주 쓰임
    이 경우 CORDIC의 요령은 전통적인 sin/cos/곱셈 계산을 피하고, 회전할 벡터 자체를 CORDIC의 입력으로 넣는 것임
    그러면 CORDIC가 sin/cos를 계산하거나 복소수 곱셈을 하지 않고도 회전된 벡터를 직접 만들어냄
    CORDIC는 지연시간이 크게 중요하지 않을 때 특히 빛남
    계산의 각 단계를 파이프라인화하면 처리량을 크게 얻을 수 있어, 무선 시스템의 디지털 믹싱에 잘 맞음

  • 2023년 기준으로 일부 현대 MCU는 저가이면서도 FPU가 있음
    STM32G4가 좋은 예이고, M0 MCU 같은 경우와 달리 고정소수점을 쓰기 싫다면 f32를 자유롭게 쓸 수 있음
    이런 칩은 MCU당 대략 1~2달러에 구할 수 있음
    다만 G4에는 고정소수점 용도로 이 알고리즘을 구현한 하드웨어 CORDIC 주변장치도 있음
    이게 주로 부동소수점 정밀도 손실을 피하려는 용도인지 궁금함
    레지스터로 프로그래밍하지만 CPU에서 직접 CORDIC를 구현하는 게 아니라 IC 내부의 전용 하드웨어가 처리함

  • 예전에 참여했던 꽤 귀여운 코드 조각이 떠오름
    단위원의 호가 만드는 각의 이등분선 좌표를 찾아야 했고, 양팔의 (x,y) 좌표는 이미 있었음
    기존 구현은 (x,y) 좌표를 극좌표 (r,θ)로 바꾸고, 계산된 θ가 올바른 사분면에 있는지 확인한 뒤, θ를 절반으로 나누고 다시 (x,y)로 변환하는 삼각함수 덩어리였음
    결과적으로 삼각함수와 역함수를 많이 호출했음
    Python이라 복소수를 1급으로 쓸 수 있었기 때문에, (x1,y1)에서 z1, (x2,y2)에서 z2라는 두 복소수를 정의하고 곱의 기하평균 √(z1*z2)만 취하면 끝났음
    새 코드에는 명시적인 삼각함수도, 명시적인 변환과 역변환도 없었음

  • “22.75˚만큼 회전하는 것은 45˚ 회전한 다음 -22.5˚ 회전하는 것과 같다는 건 꽤 명백하다”라고 되어 있는데, 그러면 22.5° 회전 아닌가?
    글의 오류인지, 내가 잘못 이해한 건지 궁금함

    • 글의 오류임
  • Meagher의 옥트리 시스템은 정수 곱셈·나눗셈 없이 정수 산술만 쓴 것으로 유명함
    “불리언 연산(합집합, 교집합, 차집합), 기하 연산(이동, 크기 조정, 회전), N차원 간섭 검출, 공간의 임의 지점에서 은면 제거를 포함한 표시를 위한 효율적인 선형 시간 알고리즘이 개발되었다. 이 알고리즘은 부동소수점 연산, 정수 곱셈, 정수 나눗셈을 필요로 하지 않는다”
    https://doi.org/10.1016/0146-664X(82)90104-6
    이 덕분에 옥트리 표현을 위한 빠른 맞춤형 VLSI 그래픽 가속 하드웨어를 만들기 쉬웠음

  • CORDIC가 작은 테이블을 쓰는 3차 보간이나 다른 다항식 보간과 비교해 어떤 성능을 내는지 궁금함
    자원이 제한된 신시사이저가 때때로 3차 보간을 쓴다고 배웠는데, 아마 CORDIC가 비교적 새로웠던 시기였을 것임
    대충 보면 CORDIC는 반복마다 정밀도 1비트를 얻으므로 계산은 더 비싸지만 공간은 다항식보다 덜 쓸 것 같음
    다만 공간 측면에서는 글에서 sin(x)용 4096개 항목 조회 테이블을 제시한 것보다 더 싸질 수 있다는 점을 강조해야 함
    대칭성 덕분에 전체 원 중 1/4만 필요함

    • 예전 게임 개발자와 데모씬 개발자들은 sin과 cos에 겨우 256개 항목 조회 테이블을 썼음
      바이트 크기 각도를 쓰면 자동으로 순환돼 편했고, 2D 게임의 회전에는 2^8이면 꽤 충분했음
      다만 부드러운 움직임을 원한다면 3D에서는 그리 멀리 가지 못함