Dynamic and polymorphic languages must attach information, such as types, to run time objects, and therefore adapt the memory layout of values to include space for this information. This is especially problematic in the case of IEEE754 double-precision floating-point numbers, which require exactly 64 bits, leaving no space for type information. The two main encodings in-use to this day, tagged pointers and NaN-tagging, either allocate floats on the heap or unbox them at the cost of an overhead when handling all other objects.
I believe the statement applies to Erlang, as floats are boxed, although I believe Erlang uses other techniques to avoid boxing and unboxing.
The proposed solution:
This paper presents self-tagging, a new approach to object tagging that can attach type information to 64-bit objects while retaining the ability to use all of their 64 bits for data. At its core, self-tagging exploits the fact that some bit sequences appear with very high probability. Superimposing tags with these frequent sequences allows encoding both 64-bit data and type within a single machine word. Implementations of self-tagging demonstrate that it unboxes all floats in practice, accelerating the execution time of float-intensive benchmarks in Scheme by 2.3×, and in JavaScript by 2.7× without impacting the performance of other benchmarks, which makes it a good alternative to both tagged pointers and NaN-tagging.
They have two proposals: using three 3-bit tags (one of them being 000) and another using two 3-bit tags (with special encoding for positive and negative 0.0).
I believe the first one is impractical for Erlang but the second may be applicable. While we may not have 3-bit tags available, I believe the paper would also apply to 4-bit tags (0111 and 1000). 3-bit tags covers floats from 2^-255 to 2^257 while a 4-bit tag would cover floats from 2^-127 to 2^129, which goes from 5.877471754111438e-39 to 6.80564733841877e38, which is still quite respectable.
It’s based on a pre-OTP-19.0 master commit but should be fully working.
It takes 4-bits from the exponent. This comment in erl_term.h explains the implementation quite good:
/* Immediate floats (ifloat)
*
* A double : SEEEEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
* S = sign bit
* E = full exponent (11 bits)
* M = mantissa (52 bits)
*
* An ifloat: eeeeeeeMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMStttt
*
* e = exponent - IFLOAT_EXP_MIN (7 bits)
* t = _TAG_IMMED1_IFLOAT (4 bits)
*
* Basically the exponent is reduced by an offset to fit in 7 bits,
* the entire word is rotated left 5 bits and the tag is added on.
*
* +0.0 and -0.0 are currently not IFLOAT's
*
* Why?
*
* +0.0 =:= -0.0 is true in Erlang and has been so since the beginning.
*
* All other immediates are equal (=:=) iff the Eterm words are equal.
* 0.0 as ifloat would be an exception to that rule, in which case we must
* check for that exception at all places where immediates are compared.
*/
:
#define IFLOAT_MIN (5.421010862427523e-20)
#define IFLOAT_MAX (1.844674407370955e19)
As I remember, the expensive comparing of immediates was the reason we decided not to ship it. But as +0.0 =:= -0.0 is now false since OTP 27.0 this might we worth revisiting.
Would you be able to include 0.0 and -0.0 as immediates? Wouldn’t 0.0 conflict with 0 (IIRC integers have the all zeroes tag)? Or zero floats would get assigned a special tag to be immediate?
Or perhaps it could be done with a rotate plus negate of the five bits? This way we can have dedicated tags for all integers and floats without overlap?
No, integers have the tag 2#1111. If I am reading the code correctly the suggested new tag for ifloats would be 2#0111.
Yes, as far as I understand that would be possible now since +0.0 =:= -0.0 is now false. At the time of that branch, the zeroes were not immediates to ensure that +0.0 =:= -0.0 would return true.