Problem with Complex{Rationals}

You should also understand that this distinction is absolutely critical for performance. It’s not just an arbitrary pedantic choice. Think about how instances of the types are actually stored and processed on the computer.

A Complex{Rational{Int}} for a+bi can be (and is) stored as four consecutive Int (64-bit integer) values: (anum,aden,bnum,bden). For operations on such quantities, since the compiler knows everything’s type and how it is stored, it can load the numerators and denominators directly into integer registers and perform arithmetic immediately with corresponding machine instructions. An array of n such values is stored as an array of 4n 64-bit integers consecutively in memory.

Conversely, if you have a Complex{Real} value, then the real and imaginary parts can be of any Real type. That means that they must each be a “box” that has a type tag (to indicate what Real type it is) followed by the actual value. Since these values may be of different sizes depending on the type, to store a Complex{Real} instance in a fixed amount of memory it must store pointers to the boxes. So, to work with a Complex{Real} value, the processor must fetch the pointers, then use the pointers to fetch the type tags, then dispatch to code that does + (for example) on the actual values for those types. Huge overhead! And an array of Complex{Real} objects must be an array of pairs of pointers to boxes — looping over the array to do some operation, since every element can be a different type you need to do the pointer-chasing and dynamic dispatch for every element.

If you have a compiled function that operates on an Complex{Real} or an array of Complex{Real}, therefore, it is completely different from code that operates on Complex{Rational{Int}}. You can’t use either one for the other. (If A is a subtype of B, it essentially means “compiled code for B can be used on instances of A” — this is the purpose of the subtype relation, to decide what code is applicable to what types.)

This is why Julia’s parametric types have the “invariant” semantics that they do. Once you get used to it, it’s quite easy to work with, and allows you to have generic types like Complex that are still fast and efficient for concrete instances.

27 Likes