Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Julia: Abstract and Concrete Types Integer versus Int8 versus Int64

Tags:

types

julia

Suppose I have an integer n that will only ever take values in [0, 10]. Should I declare it as n::Integer to be general about it, as n::Int8 or n::UInt8 to be parsimonious or n::Int64 for a 64 bit system?

Please clarify the reasons for a newbie, e.g. style, performance.

Reference: https://docs.julialang.org/en/release-0.5/manual/integers-and-floating-point-numbers/

Updated Reference (2021): https://docs.julialang.org/en/v1/manual/types/#man-abstract-types

like image 905
PatrickT Avatar asked Jun 08 '17 12:06

PatrickT


1 Answers

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.

like image 132
StefanKarpinski Avatar answered Oct 17 '22 02:10

StefanKarpinski