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

Tags:

Types

Julia