[ros-dev] [ros-diffs] [tkreuzer] 42353: asm version of DIB_32BPP_ColorFill: - Add frame pointer - Get rid of algin_draw, 32bpp surfaces must be DWORD aligned - Optimize the loop - Add comments

Michael Steil mist at c64.org
Tue Aug 4 14:35:23 CEST 2009


On 4 Aug 2009, at 03:06, Timo Kreuzer wrote:
> Alex Ionescu wrote:
>>
>> You see, because one of the #1 reasons why inline ASM loses vs C, is
>> that gcc understood the structure of my code -- it realized that I  
>> was
>> calling the function with static parameters, and integrated them into
>> the function itself.
>>
> What the compiler does not know is with what parameters this  
> function is generally called, unless you would profile the code and  
> then reuse the profiling data to recompile, but I don't think that  
> gcc supports that. So it has to rely on generic optimization. Hand  
> coded assembly can be optimized for the special usage pattern.  
> Anyway that's theory.

A compiler that does link time optimization like MSVC and LLVM will  
inline a small function like this one every time it is invoked, not  
only saving the call and the stack frame, but also to save copying  
around the arguments. If caller 1 happens to have the argument  
"iColor" in ebx, it will just inline the function in a way that it can  
work directly on ebx. And for caller 2, where iColor is in esi, it  
will inline it in a way so it can work with it in esi.

If you compile the source into an i386 binary .o file and analyze this  
one, the compiler does not know what registers hold the data *in  
general*, because you could still link it against anything. But at  
least it knows it for every single already existing invocation, and  
generate corresponding code.

But as soon as the compiler/linker knows all invocations (because you  
create the final executable, and your function is not an exported  
symbol any more), instead of optimizing by inlining every invocation,  
it can also decide to choose its own calling conventions between the n  
callers and the function and go for a register based calling  
convention, and make sure all callers prepare their arguments so that  
they end up in the right registers without the need to copy them around.

If you don't compile with a link time optimizing compiler like MSVC or  
LLVM (yet), you can choose to have your function as a "static inline"  
in a header file instead of in a C file, and the compiler won't  
require a link time optimizer in order to do the optimized inlining.

>> I tried calling the function twice -- gcc actually inlined the
>> function twice, with static parameters twice!
>>
> This function is neither static nor called with static parameters.

Not in the general case, but for every individual case, yes. And as  
soon as all possible callers are known to the toolchain, it might find  
some static property, like: A size is always divisible by four, a  
pointer is always aligned to a certain boundary, or a 64 bit argument  
will never be >= 2^32. You can't make these assumptions in assembly,  
because adding a single new caller will break your code. But in the C  
case, if you add a caller for which this property is not true any  
more, the compiler won't do this optimization any more.

>> But this proves one of the first points -- gcc will be able to  
>> analyze
>> the form of your program, and make minute optimizations that are
>> *impossible* to do in ASM. For example, it could decide "all  
>> functions
>> calling this function will store parameter 3 in ECX" and this will
>> optimize the overall speed of the entire program, not only the
>> function, plus save stack space. This is only an example of the many
>> hidden optimizations it could decide to do.
>>
> All the small optimizations "around" the function don't matter much  
> in this case. You can assume that the functions spends > 90% of the  
> time inside the loop. So the loop needs to be optimized, everything  
> else is candy.

So you are saying you only care that your rep movs is fast, and you  
don't care about the rest, because it's only 10%? In this case, please  
write your function in C, and replace only the memcpy() with inline  
asm, in order to make the code less error prone, more readable, more  
maintainable and more portable, without losing any of your goals  
(=speed in the tight loop).

But if it's really just the memcpy() that you think is slow, then you  
should fix the memcpy(), and have your compiler inline the version  
that you think is fastest. Or maybe the version it is inlining today  
is faster than what you think is faster.

I wonder, has either of you, Alex or Timo actually *benchmarked* the  
code on some sort of native i386 CPU before you argue whether it  
should be a stosb or a stosd? If not, writing assembly would be a  
clear case of "premature optimization".

>> Depending on how many times/how this function is called, gcc could've
>> done any number of register allocation and tree/loop optimizations
>> based on the code.
>>
>> Once I fooled gcc into generating "stupid" code, the output was very
>> similar, but more optimized than yours -- partly because ebp was
>> clobbered.
> I fixed the function to *not* clobber ebp. Misusing ebp is lame.

If you compile debug, you want ebp to be sane. If you compile release  
(i.e. perfomance), you usually do not care, In any case, with C code,  
you can decide whether you want debuggability or speed with a compile  
time switch. With assembly, you can not. And please don't start with  
#ifdefs.

>>  In case you're wondering, yes, gcc inlined the rep stosd.
>> However, it chose rep stosb instead, because I did not give it a
>> guarantee of alignment (that would be a simple __attribute__).
>>
> I wonder what compiler you are using then. I tried it with our  
> current RosBE with maximum optimization and it didn't do that. Same  
> with gcc 4.4.0 and  msc (I tested the one that ships with the WDK  
> 2008) also with maximum optimization for speed.

If the compiled code looks suboptimal, these should be the steps to  
take:
1. Understand whether the code is actually suboptimal, i.e. is it  
slow? Maybe the compiler uses stosb for a reason - for example because  
stosb is faster on unaligned data, and the compiler could not make any  
assumptions on the alignment of the data.
2. In case the code is bad because the compiler couldn't make an  
assumption, i.e. it did not know enough, then give it hints. GCC takes  
lots of hints.
3. In case the compiler was right, because you were making an  
assumption that cannot me made in general, then you should leave it  
the way it is, because the compiler is probably doing the right thing.
4. If the compiler is really acting stupid, compare it to other  
compilers to be really sure, and then file a bug with the compiler.
5. Refrain from hand-coding something that works around a compiler  
*performance* issue unless it is really really necessary. If you have  
to do so, clearly mark the code with a bug tracker number and state  
that this code should be reverted into C as soon as the compiler bug  
is fixed.

In short: Nowadays, compilers are really really smart. Amazingly  
smart. If you think you're better, then you're either doing it wrong,  
making assumptions that you haven't told the compiler about, or it's a  
compiler bug. In no case, try to do something by hand unless you  
perfectly understand *why* the compiler is doing it some way.

And yes, I understand that current compilers may have some limitations  
concerning SIMD instruction sets. (...although even this isn't  
strictly true any more today, if you know how to give the compiler the  
right hints and use the right intrinsics.) But this case is not one of  
these.

> I used push/pop in favour of movs to improve the readability, as it  
> doesn't really matter. If I had been up to ultra optimization, I  
> could have quenched out a few cycles more. Optimizing the loop was  
> sufficient for me.

See above: If all you want to optimize is the loop, then have C code  
with asm("rep movsd") in it, or fix the static inline memcpy() to be  
more efficient (if it isn't efficient in the first place).

> The challenge was obviously the compiler. Please let us know which  
> version of gcc you were using and with what options, it seems to be  
> way more sophisticated than all the compilers/options I know.

While I would indeed be interested in seeing the compiler output both  
of you get, because otherwise all this is pretty theoretical, I also  
think that it doesn't matter in the big picture. Even if today's  
compiler doesn't generate perfect code for a simple task as this one  
(and ReactOS' toolchain doesn't use the latest GCC today), then  
tomorrow's compiler will, and it's not worth doing any assembly any  
more today.

> You talked about compiler optimization, and what it could  
> theoretically do here and there, but the only thing that is worth  
> optimizing in this function is the loop and here you managed to get  
> a lousy rep stosb, not a stosd or even SSE stuff?

Now if we care a lot about memcpy() performance, then we could add  
many more tricks to the codebase. The memcpy() code in the "commpage"  
of Mac OS X is pretty fascinating, for example. Depending on the CPU  
type, different versions of memcpy() are in place, and some versions  
first look at the size of the region, and branch to one of three  
different implementations. Small regions use rep stosb, bigger ones  
SSE or the like. (I'm sure NT does something similar.)

If you are more into static optimization, or want to give the compiler  
hints about the typical size of the region, you could do some clever  
macro hacks that supply the typical size range as an extra argument to  
memcpy(), so that the best version can be chosen at preprocessing  
time, without the need for the size check in the compiled code - if,  
for example, DIB_32BPP_ColorFill() typically did memcpy()s of smaller  
sizes.

   Michael



More information about the Ros-dev mailing list