Cast a smaller integer to a larger integer

I have a variable x defined as u32. However, it has to be casted to u64 in its usages. My question is that should I really define x as u32 and do type casting when x is used or should I just define x as u64 in the beginning? Is there any (even tiny) benefit of defining x as u32 in my use case?

You normally choose your integer type depending on the expected size range and any requirements from functions you will be calling, so I would answer your question with another question. What made you choose to define x as a u32 in the first place?

If you know you'll only ever be using numbers up to 4 billion and most of the times x is used it'll be a u32 then I'd just do the casts for the few times it's needed as a u64. Alternatively, if you need it as a u64 all the time then maybe it should have been a u64 to begin with.

2 Likes

It depends on your use case. If you're storing it in a struct and space it tight, that might be beneficial. Sometimes 32bit math is faster than 64bit. Etc. If in doubt, measure and see.

Side note: To go from u32 to u64, you can use Into instead of casting:

    let a = 2u32;
    let b: u64 = a.into();
3 Likes

I define x as u32 because x's values are within the range of u32. However, I need to calculate id % x and id is of u64 so I need to cast x to u64. Casting x to u64 vs defining x as u64 in the beginning, will there be extra costs?

Does casting smaller integer type to larger integer type has extra costs compared to defining a larger integer type in the beginning?

I'd expect yes but extremely minor: a stack / register allocation large enough for a u64, and then a bitwise copy to that stack space / register.
Should take a couple of ns each time.

It's highly doubtful you'll notice any difference in any real world scenario because a cast from 32 to 64-bit integers is a single highly optimised instruction. It will almost certainly blend into the noise for a real app and you are more likely to get performance gains by reducing unnecessary function calls/copies or just using a better algorithm.

On a 64-bit system the compiler may even be using 64-bit registers already and just ignoring the top bits.

You may find some differences when working in extreme situations. For example, if you've got gigabytes of these u32's it'll take twice as long to just move them from RAM to the processor. You may also notice you can't process as many integers in bulk because SIMD can process more 32-bit integers in a single instruction than 64-bit integers.

5 Likes

BTW, what are the differences among different type casting methods: T::from, as and into?

From and Into are reflexive, and implementing one implies the other. Also, as I just learned, you can't implement a foreign trait on a foreign type. So generally you'll implement From<Foreign> for Native. But as is reserved for primitive types which can be cast as each other.

Implementing From will result in the Into implementation but not vice-versa. See also this documentation.

From and Into are generally intended to be lossless, infalliable conversions. as on the other hand can discard data and lead to bugs in some situations, for example casting a u64 to a usize may truncate the value on a 32-bit host. For integer casts in specific, using into() signals that there's no possible loss of data (From<u64> is not implemented for u32 for example).

More on as can be found in the reference.

2 Likes

I notice that on a 64-bit machine, the following doesn't work. I though usize is essentially u32 on 32-bit machines and u64 on 64-bit machines. So does it mean that even usize is storage-equivalent to u64 on a 64-bit machine, it is still treated as a different type. In other words, usize is NOT a type alias.

let x: usize = 1;
u64::from(x)

That is correct. If usize we're a type alias then there could be code that would compile on 32 bit architectures but not on 63 bit architectures, which would be painful.

1 Like

This could never work because if there ever is a 128-bit arch, the u64::from(x) would no longer fit. So in order to preserve future compatibility, this cannot be implemented in general.

1 Like

For a local variable, just use whatever width makes the code around it the most natural. If that's u64 in your case because that's how it's used, just do that.

Optimizing variable sizes is really only worth bothering for things of which you're storing many instances.

4 Likes

Note that the "many" here may differ from your intuition. Thausands is usually not considered many. Millions is considered many depending on the context, but not always. It's rare to not consider billions as many.

3 Likes