Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are more x86 instructions faster than less? [duplicate]

So I've been reading up about the what goes on inside x86 processors for about half a year now. So I decided to try my hand at x86 assembly for fun, starting only with 80386 instructions to keep it simple. (I'm trying to learn mostly, not optimize)

I have a game I made a few months ago coded in C, so I went there and rewrote the bitmap blitting function from scratch with assembly code. What I don't get is that the main pixel plotting body of the loop is faster with the C code (which is 18 instructions) than my assembly code (which is only 7 instructions, and I'm almost 100% certain it doesn't straddle cache line boundaries).

So my main question is why do 18 instructions take less time than the 7 instructions? At the bottom I have the 2 code snippets.

PS. Each color is 8 bit indexed. C Code:

    {
        for (x = 0; x < src.w; x++)
00D35712  mov         dword ptr [x],0                       // Just initial loop setup
00D35719  jmp         Renderer_DrawBitmap+174h (0D35724h)   // Just initial loop setup
00D3571B  mov         eax,dword ptr [x]  
00D3571E  add         eax,1  
00D35721  mov         dword ptr [x],eax  
00D35724  mov         eax,dword ptr [x]  
00D35727  cmp         eax,dword ptr [ebp-28h]  
00D3572A  jge         Renderer_DrawBitmap+1BCh (0D3576Ch)  
        {
                *dest_pixel = renderer_trans[renderer_light[*src_pixel][light]][*dest_pixel][trans];
// Start of what I consider the body
00D3572C  mov         eax,dword ptr [src_pixel]  
00D3572F  movzx       ecx,byte ptr [eax]  
00D35732  mov         edx,dword ptr [light]  
00D35735  movzx       eax,byte ptr renderer_light (0EDA650h)[edx+ecx*8]  
00D3573D  shl         eax,0Bh  
00D35740  mov         ecx,dword ptr [dest_pixel]  
00D35743  movzx       edx,byte ptr [ecx]  
00D35746  lea         eax,renderer_trans (0E5A650h)[eax+edx*8]  
00D3574D  mov         ecx,dword ptr [dest_pixel]  
00D35750  mov         edx,dword ptr [trans]  
00D35753  mov         al,byte ptr [eax+edx]  
00D35756  mov         byte ptr [ecx],al  
            dest_pixel++;
00D35758  mov         eax,dword ptr [dest_pixel]  
00D3575B  add         eax,1  
00D3575E  mov         dword ptr [dest_pixel],eax  
            src_pixel++;
00D35761  mov         eax,dword ptr [src_pixel]  
00D35764  add         eax,1  
00D35767  mov         dword ptr [src_pixel],eax  
// End of what I consider the body
        }
00D3576A  jmp         Renderer_DrawBitmap+16Bh (0D3571Bh)  

And the assembly code I wrote: (esi is the source pixel, edi is the screen buffer, edx is the light level, ebx is the transparency level, and ecx is the width of this row)

drawing_loop:
00C55682  movzx       ax,byte ptr [esi]  
00C55686  mov         ah,byte ptr renderer_light (0DFA650h)[edx+eax*8]  
00C5568D  mov         al,byte ptr [edi]  
00C5568F  mov         al,byte ptr renderer_trans (0D7A650h)[ebx+eax*8]  
00C55696  mov         byte ptr [edi],al  

00C55698  inc         esi  
00C55699  inc         edi  
00C5569A  loop        drawing_loop (0C55682h)  
// This isn't just the body this is the full row plotting loop just like the code above there

And for context, the pixel is lighted with a LUT and the transparency is done also with a LUT. Pseudo C code:

//transparencyLUT[new][old][transparency level (0 = opaque, 7 = full transparency)]
//lightLUT[color][light level (0 = white, 3 = no change, 7 = full black)]
dest_pixel = transparencyLUT[lightLUT[source_pixel][light]]
                            [screen_pixel]
                            [transparency];

What gets me is how I use pretty much the same instructions the C code does, but just less of them?

If you need more info I'll be happy to give more, I just don't want this to be a huge question. I'm just genuinely curious because I'm sorta new to x86 assembly programming and want to learn more about how our cpus actually work.

My only guess is that the out of order execution engine doesn't like my code because its all memory accesses moving to the same register.

like image 810
Napoleon Avatar asked Nov 18 '25 05:11

Napoleon


1 Answers

Not all instructions take the same time, modern implementations of the CPU can execute (parts of) some instructions in parallel (as long as one doesn't read the data written by the previous one and the required units don't collide). Latest versions do translate the "machine" instructions into lower level, very simple ones, that are scheduled on the fly to be executed on the various units in the CPU in parallel as much as possible, using a whole lot of shadow registers (i.e., one instruction can be using the value in one copy of %eax (old value) after another instruction writes a new value into another copy of %eax (new value), thus decoupling instructions even more. The hoops they jump through for performance's sake...

like image 107
vonbrand Avatar answered Nov 19 '25 19:11

vonbrand