저는 Intel Core Duo에서 핵심 수학의 일부를 프로파일 링했으며 제곱근에 대한 다양한 접근 방식을 살펴보면서 이상한 점을 발견했습니다. SSE 스칼라 연산을 사용하면 역 제곱근을 취하고 곱하는 것이 더 빠릅니다. 기본 sqrt opcode를 사용하는 것보다 sqrt를 얻으려면!
다음과 같은 루프로 테스트하고 있습니다.
inline float TestSqrtFunction( float in );
void TestFunc()
{
#define ARRAYSIZE 4096
#define NUMITERS 16386
float flIn[ ARRAYSIZE ]; // filled with random numbers ( 0 .. 2^22 )
float flOut [ ARRAYSIZE ]; // filled with 0 to force fetch into L1 cache
cyclecounter.Start();
for ( int i = 0 ; i < NUMITERS ; ++i )
for ( int j = 0 ; j < ARRAYSIZE ; ++j )
{
flOut[j] = TestSqrtFunction( flIn[j] );
// unrolling this loop makes no difference -- I tested it.
}
cyclecounter.Stop();
printf( "%d loops over %d floats took %.3f milliseconds",
NUMITERS, ARRAYSIZE, cyclecounter.Milliseconds() );
}
TestSqrtFunction에 대한 몇 가지 다른 바디로 이것을 시도했으며 실제로 머리를 긁는 타이밍이 있습니다. 최악의 상황은 기본 sqrt () 함수를 사용하고 “스마트”컴파일러가 “최적화”되도록하는 것입니다. 24ns / float에서 x87 FPU를 사용하면 이것은 비참하게 나빴습니다.
inline float TestSqrtFunction( float in )
{ return sqrt(in); }
다음으로 시도한 것은 내장 함수를 사용하여 컴파일러가 SSE의 스칼라 sqrt opcode를 사용하도록하는 것입니다.
inline void SSESqrt( float * restrict pOut, float * restrict pIn )
{
_mm_store_ss( pOut, _mm_sqrt_ss( _mm_load_ss( pIn ) ) );
// compiles to movss, sqrtss, movss
}
이것은 11.9ns / float에서 더 좋았습니다. 나는 또한 시도 카맥의 엉뚱한 뉴턴 – 랩슨 근사 기법 비록 2 1의 오류, 4.3ns / 플로트에서, 더 나은 하드웨어보다는 실행, 10 (내 목적을 위해 너무 많이).
doozy는 역수 제곱근에 대한 SSE 연산을 시도한 다음 곱셈을 사용하여 제곱근을 얻었습니다 (x * 1 / √x = √x). 이 두 개의 의존하는 작업을한다하더라도, 그것은 가장 빠른 솔루션으로까지 1.24ns / 플로트에서 정확한 2했다 -14 :
inline void SSESqrt_Recip_Times_X( float * restrict pOut, float * restrict pIn )
{
__m128 in = _mm_load_ss( pIn );
_mm_store_ss( pOut, _mm_mul_ss( in, _mm_rsqrt_ss( in ) ) );
// compiles to movss, movaps, rsqrtss, mulss, movss
}
내 질문은 기본적으로 무엇을 제공 합니까? SSE의 내장 하드웨어 제곱근 opcode가 다른 두 가지 수학 연산에서 합성하는 것보다 느린 이유는 무엇 입니까?
다음을 확인했기 때문에 이것이 실제로 작업 자체의 비용이라고 확신합니다.
- 모든 데이터는 캐시에 들어가며 액세스는 순차적입니다.
- 함수는 인라인됩니다
- 루프를 풀면 차이가 없습니다.
- 컴파일러 플래그가 전체 최적화로 설정되어 있으며 어셈블리가 양호하며 확인했습니다.
( 편집 : stephentyrone 올바르게 벡터화 SIMD를 사용해야 숫자의 긴 문자열에 대한 작업처럼 작전을 포장 지적 rsqrtps
-하지만 여기에 배열에만 목적을 테스트하기위한 것입니다 : 내가 정말 측정하려고하는 것은 스칼라 코드에서 사용하기에 성능 벡터화 할 수 없습니다.)
답변
sqrtss
올바르게 반올림 된 결과를 제공합니다. 약 11 비트까지 정확한 역수에 rsqrtss
대한 근사치 를 제공합니다 .
sqrtss
정확도가 필요할 때 훨씬 더 정확한 결과를 생성합니다. rsqrtss
근사치로 충분하지만 속도가 필요한 경우를 위해 존재합니다. Intel의 문서를 읽으면 거의 완전한 정밀도 (제대로 기억한다면 23 비트 정도의 정확도)를 제공하는 명령 시퀀스 (역수 제곱근 근사에 이어 단일 Newton-Raphson 단계)도 찾을 수 있습니다. 보다 빠릅니다 sqrtss
.
편집 : 속도가 중요하고 실제로 많은 값에 대해 루프에서 이것을 호출하는 경우 이러한 명령어의 벡터화 된 버전을 사용해야합니다. rsqrtps
또는 sqrtps
둘 다 명령어 당 4 개의 부동 소수점을 처리합니다.
답변
이것은 분열도 마찬가지입니다. MULSS (a, RCPSS (b))는 DIVSS (a, b)보다 훨씬 빠릅니다. 실제로 Newton-Raphson 반복으로 정밀도를 높이더라도 여전히 더 빠릅니다.
Intel과 AMD는 모두 최적화 매뉴얼에서이 기술을 권장합니다. IEEE-754 준수가 필요하지 않은 애플리케이션에서 div / sqrt를 사용하는 유일한 이유는 코드 가독성입니다.
답변
대답을 제공하는 대신 실제로 틀릴 수 있습니다. (저는 캐시 및 기타 항목에 대해 확인하거나 논쟁하지 않을 것입니다. 동일하다고 가정 해 보겠습니다.) 질문에 답할 수있는 출처를 알려 드리겠습니다.
차이점은 sqrt 및 rsqrt가 계산되는 방법에있을 수 있습니다. http://www.intel.com/products/processor/manuals/에서 자세한 내용을 확인할 수 있습니다 . 사용중인 프로세서 기능에 대한 읽기부터 시작하는 것이 좋습니다. 특히 rsqrt에 대한 정보가 있습니다 (cpu는 근사치가 큰 내부 조회 테이블을 사용하므로 결과를 훨씬 쉽게 얻을 수 있음). rsqrt가 sqrt보다 훨씬 빠르기 때문에 1 개의 추가 다중 작업 (비용이 많이 들지 않음)이 여기서 상황을 변경하지 않을 수도 있습니다.
편집 : 언급 할 가치가있는 몇 가지 사실 :
1. 그래픽 라이브러리에 대한 마이크로 최적화를 수행하고 벡터 길이를 계산하는 데 rsqrt를 사용했습니다. (sqrt 대신 제곱합에 rsqrt를 곱했습니다. 테스트에서 정확히 수행 한 작업입니다) 성능이 더 좋습니다.
2. 단순 조회 테이블을 사용하여 rsqrt를 계산하는 것이 더 쉬울 수 있습니다. rsqrt의 경우 x가 무한대가되면 1 / sqrt (x)가 0이되므로 x가 작 으면 함수 값이 변경되지 않습니다 (많이). sqrt-무한대로 이동하므로 간단한 경우입니다.).
또한 설명 : 내가 링크 한 책에서 어디에서 찾았는지 확실하지 않지만 rsqrt가 룩업 테이블을 사용하고 있다는 것을 읽었습니다. 결과가 나올 때만 사용해야합니다. 정확할 필요는 없지만-얼마 전과 마찬가지로 내가 틀릴 수도 있습니다. :).
답변
Newton-Raphson 은 도함수 f(x)
가 -f/f'
어디 f'
인지 와 같은 증분 을 사용하여 0으로 수렴합니다 .
의 경우 를 사용하여 x=sqrt(y)
해결 f(x) = 0
을 시도 할 수 있습니다 .x
f(x) = x^2 - y
그런 다음 증분은 dx = -f/f' = 1/2 (x - y/x) = 1/2 (x^2 - y) / x
느린 분할이 있습니다.
다른 기능 (예 :)을 시도 할 수 f(x) = 1/y - 1/x^2
있지만 똑같이 복잡합니다.
1/sqrt(y)
이제 보자 . 시도해 볼 수 f(x) = x^2 - 1/y
있지만 똑같이 복잡 dx = 2xy / (y*x^2 - 1)
합니다. 명확하지 않은 대안 f(x)
은 다음과 같습니다.f(x) = y - 1/x^2
그때: dx = -f/f' = (y - 1/x^2) / (2/x^3) = 1/2 * x * (1 - y * x^2)
아! 사소한 표현은 아니지만 곱셈 만 있고 나누기는 없습니다. => 더 빨리!
그리고 : 전체 업데이트 단계는 new_x = x + dx
다음과 같습니다.
x *= 3/2 - y/2 * x * x
그것도 쉽습니다.
답변
이미 몇 년 전부터 이것에 대한 많은 다른 답변이 있습니다. 다음은 합의가 옳은 것입니다.
- rsqrt * 명령어는 약 11-12 비트에 해당하는 역수 제곱근에 대한 근사치를 계산합니다.
- 가수로 색인 된 룩업 테이블 (즉, ROM)로 구현됩니다. (사실, 이전의 수학적 테이블과 유사한 압축 된 조회 테이블이며, 트랜지스터를 절약하기 위해 하위 비트를 조정합니다.)
- 사용 가능한 이유는 “실제”제곱근 알고리즘에 대해 FPU에서 사용하는 초기 추정치이기 때문입니다.
- 대략적인 상호 명령 인 rcp도 있습니다. 이 두 명령어는 FPU가 제곱근과 나눗셈을 구현하는 방법에 대한 단서입니다.
합의가 잘못된 점은 다음과 같습니다.
- SSE 시대의 FPU는 제곱근을 계산하는 데 Newton-Raphson을 사용하지 않습니다. 소프트웨어에서는 훌륭한 방법이지만 하드웨어에서 그렇게 구현하는 것은 실수입니다.
역수 제곱근을 계산하는 NR 알고리즘에는 다른 사람들이 언급했듯이이 업데이트 단계가 있습니다.
x' = 0.5 * x * (3 - n*x*x);
그것은 많은 데이터 의존적 곱셈과 하나의 빼기입니다.
다음은 최신 FPU가 실제로 사용하는 알고리즘입니다.
주어진 b[0] = n
경우 1 Y[i]
에 b[n] = b[0] * Y[0]^2 * Y[1]^2 * ... * Y[n]^2
접근 하는 일련의 숫자를 찾을 수 있다고 가정합니다 . 그런 다음 다음을 고려하십시오.
x[n] = b[0] * Y[0] * Y[1] * ... * Y[n]
y[n] = Y[0] * Y[1] * ... * Y[n]
명확하게 x[n]
접근 sqrt(n)
하고 y[n]
접근 1/sqrt(n)
합니다.
역수 제곱근에 대해 Newton-Raphson 업데이트 단계를 사용하여 좋은 결과를 얻을 수 있습니다 Y[i]
.
b[i] = b[i-1] * Y[i-1]^2
Y[i] = 0.5 * (3 - b[i])
그때:
x[0] = n Y[0]
x[i] = x[i-1] * Y[i]
과:
y[0] = Y[0]
y[i] = y[i-1] * Y[i]
다음 주요 관찰은 b[i] = x[i-1] * y[i-1]
. 그래서:
Y[i] = 0.5 * (3 - x[i-1] * y[i-1])
= 1 + 0.5 * (1 - x[i-1] * y[i-1])
그때:
x[i] = x[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
= x[i-1] + x[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))
y[i] = y[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
= y[i-1] + y[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))
즉, 초기 x 및 y가 주어지면 다음 업데이트 단계를 사용할 수 있습니다.
r = 0.5 * (1 - x * y)
x' = x + x * r
y' = y + y * r
또는 더 멋지게 설정할 수 있습니다 h = 0.5 * y
. 이것은 초기화입니다.
Y = approx_rsqrt(n)
x = Y * n
h = Y * 0.5
그리고 이것은 업데이트 단계입니다.
r = 0.5 - x * h
x' = x + x * r
h' = h + h * r
이것은 Goldschmidt의 알고리즘이며 하드웨어에서 구현하는 경우 큰 이점이 있습니다. “내부 루프”는 세 번의 곱하기 더하기이고 다른 것은 없으며 두 개는 독립적이며 파이프 라인 될 수 있습니다.
1999 년에 FPU는 이미 파이프 라인 된 더하기 / 빼기 회로와 파이프 라인 된 곱하기 회로가 필요했습니다. 그렇지 않으면 SSE가 “스트리밍”되지 않을 것입니다. 1999 년에는 많은 하드웨어를 제곱근으로 낭비하지 않고 완전한 파이프 라인 방식으로 내부 루프를 구현하기 위해 각 회로 중 하나만 필요했습니다.
물론 오늘날 우리는 프로그래머에게 노출 된 곱셈-더하기를 융합했습니다. 다시 말하지만, 내부 루프는 3 개의 파이프 라인 FMA로, 제곱근을 계산하지 않는 경우에도 일반적으로 유용합니다.
답변
이 명령어는 반올림 모드를 무시하고 부동 소수점 예외 또는 비정규 화 된 숫자를 처리하지 않기 때문에 더 빠릅니다. 이러한 이유로 다른 fp 명령어를 순서에 맞지 않게 파이프 라인, 추측 및 실행하는 것이 훨씬 쉽습니다.