Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

I am aware of the performance hit when mixing signed ints with floats.

Is it any worse to mix unsigned ints with floats?

Is there any hit when mixing signed/unsigned without floats?

Do the different sizes (u32, u16, u8, i32, i16, i8) have any effect on performance? On which platforms?

share|improve this question

closed as off-topic by Josh Petrie Mar 11 at 3:21

This question appears to be off-topic. The users who voted to close gave this specific reason:

  • "Programming questions that aren't specific to game development are off-topic here, but can be asked on Stack Overflow. A good rule of thumb is to ask yourself "would a professional game developer give me a better/different/more specific answer to this question than other programmers?"" – Josh Petrie
If this question can be reworded to fit the rules in the help center, please edit the question.

I've removed the PS3-specific text / tag, because this is a good question about any architecture, and the answer holds true for all architectures that separate integer and floating point registers, which is practically all of them. –  user744 Jan 31 '11 at 17:26

7 Answers 7

The large penalty from mixing ints (of any kind) and floats is because these are in different register sets. To go from one register set to the other, you have to write the value to memory and read it back, which incurs a a load-hit-store stall.

Going between different sizes or signed-ness of ints keeps everything in the same register set, so you avoid the big penalty. There may be smaller penalties due to sign-extensions, etc. but these are much smaller than a load-hit-store.

share|improve this answer
The article you linked states that the PS3 Cell Processor is an exception to this because apparently everything is stored in the same set of registers (can be found approximately in the middle of the article or search for "Cell"). –  bummzack Jan 31 '11 at 17:40
@bummzack: That applies only to the SPEs, not the PPE; the SPEs have a very, uh, special, floating point environment, and the cast is still relatively expensive. Also, the costs are still the same for signed vs. unsigned integers. –  user744 Jan 31 '11 at 18:04
Thanks Joe, you beat me to it :) –  celion Jan 31 '11 at 18:18
That's a good article and it is important to know about LHS (and I'm voting it up for that) but my question is about those sign-related penalties. I know these are small and probably negligible, but I would still like to see some real numbers or references about them. –  Luis Jan 31 '11 at 19:22
@Luis - I was trying to find some public documentation on this but can't find it at the moment. If you have access to the Xbox360 documentation, there's a good whitepaper by Bruce Dawson that covers some of this (and its very good in general). –  celion Jan 31 '11 at 20:18

I suspect that information about the Xbox 360 and PS3 specifically are going to be behind licensed-developer-only walls, like most low-level details. However, we can construct an equivalent x86 program and disassemble it to get a general idea.

First, let's see what unsigned widening costs:

unsigned char x = 1;
unsigned int y = 1;
unsigned int z;
z = x;
z = y;

The relevant portion disassembles into (using GCC 4.4.5):

    z = x;
  27:   0f b6 45 ff             movzbl -0x1(%ebp),%eax
  2b:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  2e:   8b 45 f8                mov    -0x8(%ebp),%eax
  31:   89 45 f4                mov    %eax,-0xc(%ebp)

So basically the same - in one case we move a byte, in the other we move a word. Next:

signed char x = 1;
signed int y = 1;
signed int z;
z = x;
z = y;

Turns into:

   z = x;
  11:   0f be 45 ff             movsbl -0x1(%ebp),%eax
  15:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  18:   8b 45 f8                mov    -0x8(%ebp),%eax
  1b:   89 45 f4                mov    %eax,-0xc(%ebp)

So the cost of the sign extension is whatever the cost of movsbl rather than movzbl is - sub-instruction level. That's basically impossible to quantify on modern processors due to the way the modern processors work. Everything else, ranging from memory speed to caching to what was in the pipeline beforehand, is going to dominate the runtime.

In the ~10 minutes it took me to write these tests, I could easily have found a real performance bug, and as soon as I turn on any level of compiler optimization, the code becomes unrecognizable for such straightforward tasks.

This isn't Stack Overflow, so I hope no one here will claim microoptimization doesn't matter. Games often work on data that is very large and very numeric, so careful attention to branching, casts, scheduling, structure alignment, and so on can give very critical improvements. Anyone who has spent a lot of time optimizing PPC code probably has at least one horror story about load-hit-stores. But in this case, it really doesn't matter. The storage size of your integer type doesn't affect performance, as long as it's aligned and fits in a register.

share|improve this answer
(CW because this is really just a comment on celion's answer, and because I'm curious what code alterations people might have to make it more illustrative.) –  user744 Jan 31 '11 at 22:16
Information about the PS3 CPU is readily and legally available, so discussion of CPU stuff relating to PS3 isn't a problem. Up until Sony removed OtherOS support, anyone could stick Linux on a PS3 and program it. The GPU was off limits, but the CPU (including the SPEs) are fine. Even without OtherOS support you can easily grab the appropriate GCC and see what the code-gen is like. –  JasonD Feb 1 '11 at 9:59
@Jason: I've flagged my post as CW so if someone does this they can provide the information. However, anyone with access to Sony's official GameOS compiler - which is really the only one that matters - is probably barred from doing so. –  user744 Feb 1 '11 at 11:16
Actually the signed integer is more expensive on PPC IIRC. It does have a tiny performance hit, but it is there... also a lot of the PS3 PPU/SPU details are here: jheriko-rtw.blogspot.co.uk/2011/07/ps3-ppuspu-docs.html and here: jheriko-rtw.blogspot.co.uk/2011/03/ppc-instruction-set.html. Curious what this GameOS compiler is though? Is that the GCC compier or the SNC one? iirc other than the things mentioned already the signed comparisons have an overhead when talking about optimising innermost loops. I don't have access to the docs describing this though - and even if I did... –  jheriko Apr 9 '12 at 12:06

Signed integer operations can be more expensive on nearly all architectures. For example, division by a constant is faster when unsigned, e.g:

unsigned foo(unsigned a) { return a / 1024U; }

is going to be optimized to:

unsigned foo(unsigned a) { return a >> 10; }


int foo(int a) { return a / 1024; }

will optimize to:

int foo(int a) {
  return (a + 1023 * (a < 0)) >> 10;

or on systems where branching is cheap,

int foo(int a) {
  if (a >= 0) return a >> 10;
  else return (a + 1023) >> 10;

Same goes for modulo. This also holds true for non-powers-of-2 (but the example is more complex). If your architecture doesn't have a hardware divide (e.g most ARM), unsigned divides of non-consts are also quicker.

In general, telling the compiler that negative numbers can't result will aid optimization of expressions, especially those used for loop termination and other conditionals.

As for different size ints, yes there's a slight impact but you'd have to weigh that vs moving less memory around. These days you probably gain more from accessing less memory than you lose from size expansion. You're very far into micro-optimization at that point.

share|improve this answer
I edited your optimized code to be more reflective of what GCC actually generates, even on -O0. Having a branch was misleading when a test+lea lets you do it branchless. –  user744 Feb 1 '11 at 9:42
On x86, maybe. On ARMv7 it's just conditionally executed. –  John Ripley Feb 1 '11 at 10:09

Operations with signed or unsigned int have the same cost on current processors (x86_64, x86, powerpc, arm). On 32bits processor, u32, u16, u8 s32, s16, s8 should be the same. You can have penalty with bad alignement.

But convert int to float or float to int is a costly operation. You can easily find optimized implementation (SSE2, Neon ...).

The most important point is probably memory access. If your data doesn't fit in L1/L2 cache, you will loose more cycle than conversion.

share|improve this answer

Jon Purdy says above (I can't comment) that unsigned might be slower because it can't overflow. I disagree, unsigned arithmetic is simple moular arithmetic modulo 2 to the number of bits in the word. Signed operations in principle can suffer overflows, but they are usually turned off.

Sometimes you can do clever (but not very readable things) like pack two or more data items into an int, and get multiple operations per instruction (pocket arithmetic). But you gotta understand what you are doing. Of course MMX allows you to do this naturally. But sometimes using the largest HW supported word size and manually packing the data gives you the fastest implementation.

Be careful about data alignment. On most HW implementations nonaligned loads and stores are slower. Natural alignment, means that for say a 4byte word, the address is a multiple of four, and eight byte word addresses should be multiples of eight bytes. This carries over into SSE (128bit favors 16byte alignment). AVX will soon extend these "vector" register sizes to 256bits then 512bits. And aligned loads/stores will be faster than unaligned ones. For HW geeks, an unaligned memory operation may span things like cacheline and even page boundaries, for which the HW has to be careful about.

share|improve this answer

It is slightly better to use signed integers for loop indexes, because signed overflow is undefined in C, so the compiler will assume that such loops have fewer corner cases. This is controlled by gcc's "-fstrict-overflow" (enabled by default) and the effect is probably hard to notice without reading the assembly output.

Beyond that, x86 works better if you don't mix types, because it can use memory operands. If it has to convert types (sign or zero extensions) that means an explicit load and the use of a register.

Stick with int for local variables and most of this will happen by default.

share|improve this answer

As celion points out, the overhead of converting between ints and floats has largely to do with the copying and conversion of values between registers. The only overhead of unsigned ints in and of themselves comes from their guaranteed wraparound behaviour, which necessitates a certain amount of overflow checking in the compiled code.

There is basically no overhead in converting between signed and unsigned integers. Different sizes of integer may be (infinitesimally) faster or slower to access depending on platform. Generally speaking, the size of integer that's closest to the word size of the platform will be the fastest to access, but the overall performance difference depends on many other factors, most notably cache size: if you use uint64_t when all you need is uint32_t, it may be that less of your data is going to fit in the cache at once, and you may incur some load overhead.

It's a bit excessive to even think about this, though. If you use types that are appropriate for your data, things ought to work perfectly fine, and the amount of power to be gained by selecting types based on architecture is negligible anyway.

share|improve this answer
What overflow checking are you referring to? Unless you mean a level lower than assembler, the code to add two ints is identical on most systems, and not really longer on the few that use e.g. sign-magnitude. Just different. –  user744 Feb 1 '11 at 0:35
@JoeWreschnig: Damn. I can't seem to find it, but I know I've seen examples of different assembler output accounting for defined wraparound behaviour, at least on certain platforms. The only related post I could find: stackoverflow.com/questions/4712315/… –  Jon Purdy Feb 1 '11 at 4:12
Different assembler output for different wraparound behavior is because the compiler can make optimizations in the signed case that, e.g. if b > 0 then a + b > a, because signed overflow is undefined (and thus can't be relied on). It's really a totally different situation. –  user744 Feb 1 '11 at 9:32

Not the answer you're looking for? Browse other questions tagged or ask your own question.