Skip to main content
gamedev.net
🔒 Locked

SIMD techniques.

Started by bluntman Dec 14, 2005 at 8:45 AM 10 replies 1k views
Original Post
bluntman
bluntman
I am using MS intrinsics to perform SIMD operations of sets of floats (4 element vectors ofcourse). All I am doing is adding a fixed vector to my array of vectors. They are stored as an array of structures. The code (from memory, I am not at my PC):

void ShiftGrid(Vector shift)
{
    _m128 p, v, res;

    _mm_load_ps(v, &shift.x);

    float *pdata = m_pGrid;

    for(int i=0; i<gridsize; i++, pdata+=4)
    {
        p = _mm_load_ps(pdata);
        res = _mm_add_ps(p, v);
        _mm_store_ps(pdata, res);
    }
}


This works fine, just want some tips on speeding it up. The data is generally about 16000 vectors. I know you can stream data from memory without going via the cache, and also force data to stream to the cache, but I don't know which is faster in this case, or quite how to implement either. Thanks!
vNistelrooy
vNistelrooy
void ShiftGrid(Vector shift){    _m128 p0,p1,p2,p3,v;    _mm_load_ps(v, &shift.x);    float *pdata = m_pGrid;    for(int i=0; i<gridsize; i+=4, pdata+=16)    {        p0 = _mm_load_ps(pdata);        p1 = _mm_load_ps(pdata+4);        p2 = _mm_load_ps(pdata+8);        p3 = _mm_load_ps(pdata+12);        p0 = _mm_add_ps(p0,v);        p1 = _mm_add_ps(p1,v);        p2 = _mm_add_ps(p2,v);        p3 = _mm_add_ps(p3,v);        _mm_store_ps(pdata, p0);        _mm_store_ps(pdata+4, p1);        _mm_store_ps(pdata+8, p2);        _mm_store_ps(pdata+12, p3);    }}

"C lets you shoot yourself in the foot rather easily. C++ allows you to reuse the bullet!"
bluntman
bluntman
Okay guess thats one :). Does this interact especially well with SIMD (and if so why) or is it just a standard inner loop expansion?
vNistelrooy
vNistelrooy
You usually don't want to execute two *ps instructions on the same xmm* register in a row.
"C lets you shoot yourself in the foot rather easily. C++ allows you to reuse the bullet!"
bluntman
bluntman
Okay thanks, thats good to know. How about cache usage and streaming, any advice there?
Also I've heard that using SIMD forces the FPU to switch modes (all the rest of my code is standard floating point stuff), which is fairly costly, so do I need to do anything to enforce FPU mode during the function, or will it not matter?
I would imagine FPU instructions in other threads would cause an FPU mode switch aswell, how do people get around that?
bluntman
bluntman
Yeah I already got that done, its all aligned up nice!
What I really want some one to explain is the cache usage in this situation, and how I can control it to my benefit.
Tinyn
Tinyn
If you are using intrinsics, you don't have to worry about it. The compiler is taking care of cache related stuff on its own, and its usually pretty good.

The cache activity is pretty simple. Your only reading from one array. Between the compiler and the chips own automatic cache controls, its done.
Spoonbender
Spoonbender
Keep in mind that most SSE (or floating point instructions in general) have a latency of at least 3-4 cycles. (addps is 4 cycles on Athlon 64, and a good chunk more on P4) So try to ensure you have enough independent instructions to fill in those gaps. (After your addition, it takes 4+ cycles before the result is available to other instructions.
So one of the most important optimizations is to eliminate so many dependencies that this gap can be filled with other instructions (for example by unrolling the loop a few times)
bluntman
bluntman
This is my function:
	__m128 shift128, pt128_1, pt128_2, pt128_3, pt128_4;	for(i=0; i<fours; i++, bpbuff+=16)	{		pt128_1 = _mm_load_ps(bpbuff);		pt128_2 = _mm_load_ps(bpbuff+4);		pt128_3 = _mm_load_ps(bpbuff+8);		pt128_4 = _mm_load_ps(bpbuff+12);		pt128_1 = _mm_add_ps(pt128_1, shift128);		pt128_2 = _mm_add_ps(pt128_2, shift128);		pt128_3 = _mm_add_ps(pt128_3, shift128);		pt128_4 = _mm_add_ps(pt128_4, shift128);		_mm_store_ps(bpbuff, pt128_1);		_mm_store_ps(bpbuff+4, pt128_2);		_mm_store_ps(bpbuff+8, pt128_3);		_mm_store_ps(bpbuff+12, pt128_4);	}

This is the disassembly of my function:
	for(i=0; i<fours; i++, bpbuff+=16)0049C310  mov         dword ptr ,0 0049C317  jmp         WW::LOD::PatchGrid::ShiftGrid+10Bh (49C32Bh) 0049C319  mov         eax,dword ptr  0049C31C  add         eax,1 0049C31F  mov         dword ptr ,eax 0049C322  mov         ecx,dword ptr [bpbuff] 0049C325  add         ecx,40h 0049C328  mov         dword ptr [bpbuff],ecx 0049C32B  mov         eax,dword ptr  0049C32E  cmp         eax,dword ptr [fours] 0049C331  jge         WW::LOD::PatchGrid::ShiftGrid+25Ah (49C47Ah) 	{		pt128_1 = _mm_load_ps(bpbuff);0049C337  mov         eax,dword ptr [bpbuff] 0049C33A  movaps      xmm0,xmmword ptr [eax] 0049C33D  movaps      xmmword ptr [ebp-450h],xmm0 0049C344  movaps      xmm0,xmmword ptr [ebp-450h] 0049C34B  movaps      xmmword ptr [pt128_1],xmm0 		pt128_2 = _mm_load_ps(bpbuff+4);0049C352  mov         eax,dword ptr [bpbuff] 0049C355  add         eax,10h 0049C358  movaps      xmm0,xmmword ptr [eax] 0049C35B  movaps      xmmword ptr [ebp-430h],xmm0 0049C362  movaps      xmm0,xmmword ptr [ebp-430h] 0049C369  movaps      xmmword ptr [pt128_2],xmm0 		pt128_3 = _mm_load_ps(bpbuff+8);0049C370  mov         eax,dword ptr [bpbuff] 0049C373  add         eax,20h 0049C376  movaps      xmm0,xmmword ptr [eax] 0049C379  movaps      xmmword ptr [ebp-410h],xmm0 0049C380  movaps      xmm0,xmmword ptr [ebp-410h] 0049C387  movaps      xmmword ptr [pt128_3],xmm0 		pt128_4 = _mm_load_ps(bpbuff+12);0049C38E  mov         eax,dword ptr [bpbuff] 0049C391  add         eax,30h 0049C394  movaps      xmm0,xmmword ptr [eax] 0049C397  movaps      xmmword ptr [ebp-3F0h],xmm0 0049C39E  movaps      xmm0,xmmword ptr [ebp-3F0h] 0049C3A5  movaps      xmmword ptr [pt128_4],xmm0 		pt128_1 = _mm_add_ps(pt128_1, shift128);0049C3AC  movaps      xmm0,xmmword ptr [shift128] 0049C3B0  movaps      xmm1,xmmword ptr [pt128_1] 0049C3B7  addps       xmm1,xmm0 0049C3BA  movaps      xmmword ptr [ebp-3D0h],xmm1 0049C3C1  movaps      xmm0,xmmword ptr [ebp-3D0h] 0049C3C8  movaps      xmmword ptr [pt128_1],xmm0 		pt128_2 = _mm_add_ps(pt128_2, shift128);0049C3CF  movaps      xmm0,xmmword ptr [shift128] 0049C3D3  movaps      xmm1,xmmword ptr [pt128_2] 0049C3DA  addps       xmm1,xmm0 0049C3DD  movaps      xmmword ptr [ebp-3B0h],xmm1 0049C3E4  movaps      xmm0,xmmword ptr [ebp-3B0h] 0049C3EB  movaps      xmmword ptr [pt128_2],xmm0 		pt128_3 = _mm_add_ps(pt128_3, shift128);0049C3F2  movaps      xmm0,xmmword ptr [shift128] 0049C3F6  movaps      xmm1,xmmword ptr [pt128_3] 0049C3FD  addps       xmm1,xmm0 0049C400  movaps      xmmword ptr [ebp-390h],xmm1 0049C407  movaps      xmm0,xmmword ptr [ebp-390h] 0049C40E  movaps      xmmword ptr [pt128_3],xmm0 		pt128_4 = _mm_add_ps(pt128_4, shift128);0049C415  movaps      xmm0,xmmword ptr [shift128] 0049C419  movaps      xmm1,xmmword ptr [pt128_4] 0049C420  addps       xmm1,xmm0 0049C423  movaps      xmmword ptr [ebp-370h],xmm1 0049C42A  movaps      xmm0,xmmword ptr [ebp-370h] 0049C431  movaps      xmmword ptr [pt128_4],xmm0 		_mm_store_ps(bpbuff, pt128_1);0049C438  movaps      xmm0,xmmword ptr [pt128_1] 0049C43F  mov         eax,dword ptr [bpbuff] 0049C442  movaps      xmmword ptr [eax],xmm0 		_mm_store_ps(bpbuff+4, pt128_2);0049C445  movaps      xmm0,xmmword ptr [pt128_2] 0049C44C  mov         eax,dword ptr [bpbuff] 0049C44F  add         eax,10h 0049C452  movaps      xmmword ptr [eax],xmm0 		_mm_store_ps(bpbuff+8, pt128_3);0049C455  movaps      xmm0,xmmword ptr [pt128_3] 0049C45C  mov         eax,dword ptr [bpbuff] 0049C45F  add         eax,20h 0049C462  movaps      xmmword ptr [eax],xmm0 		_mm_store_ps(bpbuff+12, pt128_4);0049C465  movaps      xmm0,xmmword ptr [pt128_4] 0049C46C  mov         eax,dword ptr [bpbuff] 0049C46F  add         eax,30h 0049C472  movaps      xmmword ptr [eax],xmm0 	}


There are 7 xmm registers but my compiler is only making code that uses 2! (I'm using MSVC++ 2003).
Any way to make this better, or do I have to do the asm myself?
Spoonbender
Spoonbender
Not sure how to do this with compiler intrinsics, but you may want to use the load-execute form of addps. (Where the source operand is a memory address instead of an xmm register).
Saves you some instructions/latency as you don't have to do the two instructions separately (and wait for the load to complete before you can perform the add).

Quote:
There are 7 xmm registers but my compiler is only making code that uses 2! (I'm using MSVC++ 2003).

Must be because the compiler believes there's nothing to be gained by using the remaining 6. And it's usually right. In other words, if you manually wrote the asm, where the only change was to force it to use more registers, you'd probably see little to no improvement in performance. If there's a dependency that forces your program to wait for the previous instruction to complete anyway, then it might as well reuse the same register.

Try using array notation instead of a pointer for pdata.
Makes it easier for the compiler to figure out dependencies and aliasing. (It may detect some false dependencies because your stores and loads all use the same pointer.)

Finally, which compiler settings do you use? Are optimizations enabled?

Oh, and one final thing.
There's no point in reusing the same variable name as both operand and return value for your sse operations. (pt128_1 = _mm_add_ps(pt128_1, shift128), for example).
You might be better off storing the result into a different temp variable. The compiler can easily combine them back into the same physical register if it decides it's worth it. But it's trickier to go the opposite way (deciding that if a variable is used independently in two different places, they can be given different registers. Again, it might be creating name dependencies preventing the compiler from optimizing.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.