Julia: Int versus Int8 versus Int64
It's important to distinguish two different cases.
Storage: If you have a type that stores n
as one of its fields, or as a value in an array, then you should definitely consider using Int8
or UInt8
. Even if the saved space for a single value is negligible, if many instances of your type are created and stored in a collection, then the space savings can rapidly become significant. Let's say you have a Foo
type with a field n
, then you might do this:
struct Foo
n::UInt8
end
When a value is assigned to the n
field of a Foo
object, it will automatically be converted to UInt8
and an error will be raised if the value cannot be converted faithfully:
julia> Foo(123) # Ints are automatically converted to UInt8
Foo(0x7b)
julia> typeof(ans.n)
UInt8
julia> Foo(500) # if too large, an error is raised
ERROR: InexactError()
Stacktrace:
[1] Foo(::Int64) at ./REPL[1]:2
julia> Foo(-1) # ditto if too small
ERROR: InexactError()
Stacktrace:
[1] Foo(::Int64) at ./REPL[1]:2
julia> Foo(2π/π)
Foo(0x02)
If the value that's assigned is already of the correct type then no check is required so there's no overhead.
Dispatch: If you are writing a method of a function that takes n
as an argument, then there's no harm in having as loose a type annotation on the n
argument as makes sense semantically. In the case you've described, it seems that any kind of integer value would be sensible, so using n::Integer
would probably be appropriate. For example, if wanted to implement a checked constructor for Foo
objects, you could do this:
struct Foo
n::UInt8
function Foo(n::Integer)
0 <= n <= 10 || throw(ArgumentError("n not in [0, 10]: $n"))
return new(n)
end
end
Now an error is thrown if a value outside of [0, 10] is given:
julia> Foo(123)
ERROR: ArgumentError: n not in [0, 10]: 123
Stacktrace:
[1] Foo(::Int64) at ./REPL[26]:2
julia> Foo(3)
Foo(0x03)
This Foo
construction works for any kind of integer, checking that it's in the correct range, and then converting to UInt8
. This is slightly more restrictive than the built-in constructor for Foo
, which will happily take any kind of n
argument and try to convert it to UInt8
– even when the argument is not of an integer type. If that kind of behavior is desirable, you could loosen the type signature here further to n::Real
, n::Number
(or even n::Any
, although that seems excessive).
Note that there is no performance advantage to tightly typed method arguments – the specialized code for actual argument types is generated on demand anyway.
EDIT: refer to Stefan's accepted answer. I meant this is a response to the use of types in function dispatch, but in fact contradicted myself (as I do say explicitly that function dispatch should actually be Integer).
I'd always use Int
, just for the generality of it, but it depends how performance-critical your application is. Never Int64
unless you explicitly need it. Many functions dispatch on Int
rather than Integer
(though the advice is to dispatch on abstract types), which means that they will fail when passed a UInt8 (because Int
is a subtype of Signed
and UInt
isn't) so being overzealous with types will cause problems.
As a general rule-of-thumb you should never be more specific with types than you have to.
For a newcomer (like myself) reading the abstract type docs cleared things up. In addition, checking the following helped (<:
reads 'is a subtype of'):
julia> Int<:Integer
true
julia> UInt<:Integer
true
and
julia> Int64<:Int
true