Skip to main content
GameDev.net gamedev.net
🔒 Locked

Cogito ergo (optimi)sum ?

Started by Emmanuel Deloget Sep 9, 2004 at 8:02 PM 7 replies 3.8k views
Original Post
Emmanuel Deloget
Emmanuel Deloget
Hello everybody, {edit 09/10/2004: minor corrections, added the "array access" part} Be aware that this is a rather long post. I noticed that there were a lot of posts about code optimisations in the last days . Most of them are clearly dedicated to a particular problem (either having a faster sin/cos/sqrt/whatever or dealing with the classical “what is the faster” issue). Only a few of them are really dealing with real optimisations – I mean : algorithm optimisations.

introduction

Given my personal experience, I would say that optimising your code is not really useful. It usually take a long time to get a 2% speedup, and since time is money, it should not be done unless you really need this 2% speedup. Optimising your algorithm can really improve things. This is not that difficult, since most optimisations are in fact some kind of code organisation change. Sometime, you’ll want to completely change your algorithm in order to get one which is better/faster/more robust/etc. As a professional programmer, I know you don’t do this often – it costs money :/ Anyway, even algorithm optimisations should not be done unless you know it is needed. You must first write working and maintainable code. Then, once everything is tested and works, you may want to optimise the slow parts of your program. I want this thread to be useful for people that are currently in the optimisation process. This “guide” is not intended to be a “how to write optimised code guide”. These are advices and micro-tricks that will solve micro-problems you may have. I will not deal here with any sort of large scale optimisation techniques. Be aware that most optimisations have pros and cons. And read Antareus post below :)

on the use of float and double

double are either 64 bits or 80 bits, depending on your compiler settings. IEEE floats are 32 bits. They fit in a general purpose register. The FPU is happy with them. You can use them in SSE code. So : don’t use double – unless necessary, of course, and as a side note if a code needs double then it shouldn’t need code optimisations – use floats. Anything below this line will consider that you are using floats. Your compiler may allow you to use float versions of most of the math library functions. These float versions are usually faster than their double equivalents. They are post-fixed with an “f” : sinf(), cosf(), sqrtf() and so on. For the sake of clarity, I’ll still use the non-float function name – you’ll have to shake your head and say “doh ! I remember he wants to use only floats ! So he do not mean sqrt() here, but sqrtf()”. Simple :)

converting from float to ints

When you cast a float to an int, the compiler implicitly calls the ftol() function. ftol() performs a lot of check in order to avoid NaN and other bad float problems. The point is that ftol() is slow. You can rewrite your own ftol() using the FPU – this is only 3 asm instructions – or using a pure integer technique. Of course, you’ll loose all the goodies of ftol() and you may have some weird problems with your newly computed integer value (but hey, that’s the trade between precision and accuracy :P). For those interested, see this site.

sin() and cos()

Most sin/cos optimisations come from the good old demo age, when using the FPU was considered as blatantly stupid. Remember that today’s processors are a lot faster than a 8087. Let’s name it: the sin/cos lookup table. I saw some implementations of this one in recent threads, and I have to assume that it is not dead. Surprising ^_^. A correct sin/cos LUT (let’s call it SINLUT for short) do not use a degree index. If you LUT has k x 180 entries (with k = integer > 0) then you won’t be able to achieve very good performances with your LUT, because you’ll need a costly modulus operator. A better solution is to use a LUT with 2^n entries, because of one of the characteristics of 2^n numbers: whatever k (integer, >0) is, k AND ((2^n)-1) < 2^n. The AND operator is very fast and can be paired with a lot of other instructions on our intel processors. Now, what should we use as “n” ? n should be chosen so than 2^n > 360. The precision you’ll get with 256 values for 360° is not that interesting. Remember that is you sin/cos precision is too low then you may have some precision issue in your code, and those may be hard to track and to debug. n = 9 should be ok in most cases. n=10 would create a 4096 byte-long LUT (sizeof(float)*1024) which is also the size of a Windows memory page. There is no speed gain between 512 and 1024 entries (if you manage to have all your 1024 values in a single memory page ; if you fail to achieve that, you may get some page fault penalties (see below) when looping through the array) but you’ll gain additional precisions.

sqrt()

The C library square root usually uses the FPU. You may be able to use the SSE reciprocal square root when available (RSQRT, which gives you 1/sqrt(x) – and remember that x / sqrt(x) = sqrt(x)). You may also want to use a lookup table too – I’m not sure it is that interesting. After all, the input of the sqrt() function can be really big : what if your array do not handle it ?

fabs()

fabsf() gives you the absolute value of a float. The absolute value of a float can be obtained by zeroing the sign bit of the float – which appear to be the most significant bit of the float. Therefore, an inlined function which only does

int f = (*(int *)(&myFloat)) & 0x7FFFFFFF;
return *(float *)(&f);
Will probably be faster than a plain old fabsf() call.

array access

Memory access is fast. Well, not exactly. Memory access seems to be fast, unless you run into cache miss or even worse : page faults. On Windows NT4/2k/XP, page faults are not these crappy Win9x crashes. A page fault is a kernel exception which tells the kernel that the needed memory page is not currently available to the process - the corresponding PTE valid bit is clear (... what did he said ? oh, sorry : check here and here for further informations about PTEs). The system then need to load this new page and setup some internal informations. This is slow as hell. [as a side note : starting from WinNT4, the default memory pages size is 4KB]. Cache miss are simpler - this is more a processor problem : the data you want to read/write is not in the processor data cache, therefore the processor data cache needs a refill before you can use it. This is still a very costly information. Now you perhaps wonder what I really want to say. Consider this array (silly, I know, but an array is still an array):

float array[1024][1024];
You can iterate through this array using either (code1) or (code2).

// code1
for (int x=0; x<1024; x++) {
  for (int y=0; y<1024; y++) {
    array[x][y] = some_computed_value(x, y);
  }
}

// code2
for (int y=0; y<1024; y++) {
  for (int x=0; x<1024; x++) {
    array[x][y] = some_computed_value(x, y);
  }
}
Both will work and the algorithm is the very similar. But (code2) is a lot slower than (code1): it doesn't respect the processor data cache at all - writing 4 bytes every 4096 bytes (a memory page!), then iterates. It multiplies the number of cache misses AND the number of page fault. Conclusion : linear access to memory is a lot faster than random memory access.

next ?

Other posters around there may have some optimisation tricks too –hope they’ll share their knowledge with us :). Anyway, I want you to remember my introduction before doing anything with these advices : optimisation is the last thing you’ll have to do, and all the stuff I presented here are micro-optimisations. They should help in some particular cases. But they won’t help you to correct your crappy terrain engine code :D. And I repeat : Be aware that most optimisations have pros and cons. See you tomorrow, [Edited by - Emmanuel Deloget on September 10, 2004 11:53:49 AM]
Extrarius
Extrarius
Quote:
Original post by Emmanuel Deloget
[...]

fabs()


fabsf() gives you the absolute value of a float. The absolute value of a float can be obtained by zeroing the sign bit of the float – which appear to be the most significant bit of the float. Therefore, an inlined function which only does
int f = (*(int *)(&myFloat)) & 0x7FFFFFFF;return *(float *)(&f);

Will probably be faster than a plain old fabsf() call.
[...]
Be aware that any such 'inline function' will only be faster if the number is in memory and not being used in math. If you're doing something like f2 = (fabsf(f1 + 0.5) * 10.1), and use a custom fabsf function, the number will go from memory, to the FPU, to memory, to the FPU, to memory, which isn't so great performance-wise. the default fabsf will probably be changed to an intrinsic(with the proper compiler settings), which means it'll be one(I think) FPU instruction and won't require an extra FPU->Memory->FPU step.

You have to be careful about tricks that touch memory, because going between memory and registers is often much slower than just staying in a register, and you have to make sure the time you save is greater than the time you waste.
"Walk not the trodden path, for it has borne it's burden." -John, Flying Monk
Emmanuel Deloget
Emmanuel Deloget
Quote:
Original post by Extrarius
Be aware that any such 'inline function' will only be faster if the number is in memory and not being used in math. If you're doing something like f2 = (fabsf(f1 + 0.5) * 10.1), and use a custom fabsf function, the number will go from memory, to the FPU, to memory, to the FPU, to memory, which isn't so great performance-wise.


I did not thought about that when I originally wrote this. It's true that it looks like a non optimal case. Well, I guess it perfectly fits in the Be aware that most optimisations have pros and cons comment :)

Thanks a lot,
antareus
antareus
Micro-optimizations are way easier and fun to think about than coming to the realization that: (choose one or more)

a. your design is bad
b. your algorithm is bad
c. you don't know what is bad
d. you don't feel like coding a certain portion of your project

It is human nature to look for the quick fix or a distraction. Too bad neither one of them helps finish a project.
--God has paid us the intolerable compliment of loving us, in the deepest, most tragic, most inexorable sense.- C.S. Lewis
fractoid
fractoid
Have only read half of the original post, but - can someone make this sticky? All this talk of saving one or two CPU cycles here or there for a few % speedup when you could fix your algorithm and get a 100x speedup - God I wish I could force everyone at my work to read this. Shorter variable names do *not* make your code faster FFS!!!
joanusdmentia
joanusdmentia
With the sin/cos lookup table, I was under the impression that the memory accesses invovled were slower than just using the FPU.
"Voilà! In view, a humble vaudevillian veteran, cast vicariously as both victim and villain by the vicissitudes of Fate. This visage, no mere veneer of vanity, is a vestige of the vox populi, now vacant, vanished. However, this valorous visitation of a bygone vexation stands vivified, and has vowed to vanquish these venal and virulent vermin vanguarding vice and vouchsafing the violently vicious and voracious violation of volition. The nly verdict is vengeance; a vendett o
Emmanuel Deloget
Emmanuel Deloget
Quote:
Original post by joanusdmentia
With the sin/cos lookup table, I was under the impression that the memory accesses invovled were slower than just using the FPU.


I think it depends on what you are doing. If you use only a few sin/cos, then you don't really need to optimize these few calls, since the cost of page miss will be fairly high. If you do a lot of subsequent sin/cos call, then having a LUT would help, since you'll get only a few page miss.

@everybody : thanks for your comments.

Regards,
Dmytry
Dmytry
As about sin and cos.....i doubt someone there really need to optimize it.

In 99% of times i see usage of sine and cosine on that forum ,it's something like:

Programmer have sine and cosine of some angle from the beginning. He don't have the angle.

Then he does atan2 to get the angle or he don't know atan2 and writes very buggy code, or he don't know what to do and someone suggests him to use atan2 and then sin/cos.

Then he uses the angle to get sine and cosine of it back, sometimes in the loop.

and then he maybe wants to optimize things and writes buggy or in best case just slow look-up table.

Really, in most cases you can do things without any sine or cosine. Angles may be evil - they may be clockwise, may be counterclockwise, may count from +x axis or +y axis, and may do different things if coordinate system is mirrored.....

long real-life story about angles:

I once had to write wrapper for loader of some 2d vector-graphics format. (for 50$ (that was first job as programmer! :-)). There was a parser that loaded the format into tree stored in the ram, and there was a company code that stored things in some internal format(also in ram). And they wanted to somehow convert things from loader results to their internal format(and i did and got my 50$). Both company code and loader code used angles for orientation of things. They both was poorly documented and used different conventions (one in radians other in degrees,one clockwise from vertical,other counterclockwise from +x axis,or something like that. Things worked really strange when things is mirrored (there was 4 ways to mirror thing: via bool mirrX, bool mirrY, or via negative xscale and yscale !) )

I spent roundly 3 times more that i expected (mainly because i had to use "trial-and-error" method, because of lack of documentation).It would be alot simpler for them if they used something reasonable that have single and solid convention, e.g. complex numbers . ( rotation and scale by multiplication of complex numbers, mirroring _only_ by conjugate. Or rotation,mirroring, and scaling by 2x2 matrix). I expected thing to be really simple(and 50$ to be reasonable price), and spent alot more time, because of lack of single convention and lack of documentation. They also had problems mantaining it. Additionally to unmantainability, their code was/is wastly inefficient due to sins and coss spreaded everywhere.

Same is alot more true for 3D.

[Edited by - Dmytry on September 10, 2004 1:14:12 PM]

Topic Locked

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

Sign in to reply to this topic.