Optimized x86 assembly stuff and tricks.
Most people thinks that nowadays you can't optimize better than the compiler. This is true for most parts of your program. Some function can still be optimized with hand-crafted assembly when needed. Here you'll find some functions I've created approximations for or converted to assembly to speed them up.
Fast powf() for computation of specular intensity
Fast roundf(), floorf(), ceilf() and truncf() functions
A shufps immediate value calculator
References
- Fast powf() for computation of specular intensity:
In the phong lighting model the intensity of specular highlights is calculated using the power function a^b. Usually numbers up to 128 are used for b - the higher the exponent, the sharper/smaller the highlight.
The standard function is powf(). Here we use the famous approximation by C. Schlick [1] which is a^b ~= a / (b - a * b + a) and gets fairly close to the original.
The MSVC compiler does very well when optimizing the function in release mode, but you can still gain a bit of performance with a bit of work.
In the assembler code we get from the compiler we remove the last 2 instructions, which are redundant anyway and reorder the fld instructions which saves us a fxch.
float powf_schlick(const float a, const float b)
{
return (a / (b - a * b + a));
}
float powf_schlickASM(const float a, const float b)
{
__asm {
// floating point stack
fld dword ptr [b] // = b
fmul dword ptr [a] // = b*a
//fxch
fsubr dword ptr [b] // = b-b*a
fadd dword ptr [a] // = b-b*a+a
fdivr dword ptr [a] // = a /(b-b*a+a)
//fstp dword ptr [esp+8] // empty
//fld dword ptr [esp+8] // pow(a, b)
}
}
You can also convert this function to SSE to process floating point vectors really fast:
void powf_schlickSSE(const float * a, const float b, float * result)
{
__asm {
mov eax, a //load address of vector
movss xmm0, dword ptr [b] //load exponent into SSE register
movups xmm1, [eax] //load vector into SSE register
shufps xmm0, xmm0, 0 //shuffle b into all floats
movaps xmm2, xmm1 //duplicate vector
mov eax, result //load address of result
mulps xmm1, xmm0 //xmm1 = a*b
subps xmm0, xmm1 //xmm0 = b-a*b
addps xmm0, xmm2 //xmm2 = b-a*b+a
rcpps xmm0, xmm0 //xmm1 = 1 / (b-a*b+a)
mulps xmm2, xmm0 //xmm0 = a * (1 / (b-a*b+a))
movups [eax], xmm2 //store result
}
}
The SSE function is as fast as the single float function! By using a reciprocal and then a multiply we loose a bit of precision, but in the desired range there is no problem with that.
Here are some values measured on a Intel Pentium D 3.4GHz made with 100*10ˆ6 random numbers in the range [0,128]:
Method |
Values |
Execution time |
Median error |
Maximum error |
powf() |
a ε [0,1] b ε [1,128] |
382.8ns |
0.0 |
0.0 |
powf_schlick() |
a ε [0,1] b ε [1,128] |
26.6ns |
< 0.01 |
˜0.2 |
powf_schlickASM() |
a ε [0,1] b ε [1,128] |
24.1ns |
< 0.01 |
˜0.2 |
powf_schlickSSE() |
a ε [0,1] b ε [1,128] |
28.8ns (7.2ns per float) |
< 0.01 |
˜0.2 |
With my Pentium M745 (1.8GHz) the results look a bit different. The ASM method is a bit slower on that processor and the default powf() is much faster. But the SSE function is actually faster while operating on 4 floats simultaneously:
Method |
Values |
Execution time |
Median error |
Maximum error |
powf() |
a ε [0,1] b ε [1,128] |
249.8ns |
0.0 |
0.0 |
powf_schlick() |
a ε [0,1] b ε [1,128] |
29.1ns |
< 0.01 |
˜0.2 |
powf_schlickASM() |
a ε [0,1] b ε [1,128] |
29.7ns |
< 0.01 |
˜0.2 |
powf_schlickSSE() |
a ε [0,1] b ε [1,128] |
26.8ns (6.7ns per float) |
< 0.01 |
˜0.2 |
Here is an animated image comparing the real powf() (grey) to the approximation (red). The error is shown in green: (a ε [0,1] and b ε [1, 2, 4, 8, 16, 32, 64, 128])
↑ back to top
- Fast roundf(), floorf(), ceilf() and truncf() functions:
Often you're in need of a fast floor() function for your algorithms. One way is to load a number, change the FPU control word, store the number to int and restore the original control word.
There are faster methods though. This one is from Laurent de Soras [2] and uses pure FPU code.
It rounds, floors, ceils or truncs independent of the current rounding mode. Read his PDF for the other functions and extensive information on rounding.
float floorf_ASM(const float a)
{
const float roundTowardsNI = -0.5f; //use 0.5f to round to nearest integer
float result;
__asm {
fld dword ptr [a]
fadd st, st
fadd roundTowardsNI
fistp dword ptr [result]
//return float value
sar dword ptr [result], 1
fld dword ptr [result]
/*or: return int value
mov eax, dword ptr [result]
sar eax, 1
*/
}
}
float ceilf_ASM(const float a)
{
const float roundTowardsPI = -0.5f;
float result;
__asm {
fld dword ptr [a]
fadd st, st
fsubr roundTowardsPI
fistp dword ptr [result]
//return float value
sar dword ptr [result], 1
neg dword ptr [result]
fld dword ptr [result]
/*or: return int value
mov eax, dword ptr [result]
sar eax, 1
neg eax
*/
}
}
Laurent also present methods for trunc and round to nearest, but those can be done with functions already existant in SSE which are probably faster:
float trunc_SSE(const float a)
{
float result;
__asm {
cvttss2si eax, dword ptr [a]
//cvttsd2si eax, qword ptr [a] //for double values
//return float value. if you want to return an int, you're actually done already...
mov dword ptr [result], eax
fld dword ptr [result]
}
}
float round_SSE(const float a)
{
float result;
__asm {
cvtss2si eax, dword ptr [a] //round according to rounding control bits in the MXCSR register
//cvtsd2si eax, qword ptr [a] //for double values
//return float value. if you want to return an int, you're actually done already...
mov dword ptr [result], eax
fld dword ptr [result]
}
}
Another trunc variant comes with SSE3, which has its own FPU instruction for this purpose and might be faster in some scenarios:
float trunc_SSE3(const float a)
{
float result;
__asm {
fld dword ptr [a]
fisttp dword ptr [result]
//fisttp qword ptr [result] //for double values
//return float value
fld dword ptr [result]
/*or: return int value
mov eax, dword ptr [result]
*/
}
}
Sadly I have no timing values yet...
↑ back to top
- A shufps immediate value calculator
The shupfs instruction always annoys the hell out of me. You need it often, but I usually get the immediate value wrong though I know how it works.
Or when reading some SSE code I always have to figure out what goes where...
Use the combo boxes or change the text field value to see the results.
source |
3 |
2 |
1 |
0 |
| | |
destination |
3 |
2 |
1 |
0 |
|
shufps destination, source, 0x |
|
| | | | | | | |
destination |
|
|
|
|
| | |
The great line drawing JavaScript is from p01. Thanks a lot man!
↑ back to top
|