Are there any existing implementation of NaN tagging in Nim?
(I'm about to do it myself - or at least experiment with to see if there are any performance benefits - but having sth for reference wouldn't be bad at all :))
Do you see anything wrong (particularly memory-safety-wise) with this code?
let v = Value(kind: stringValue, s: "hello")
let z = cast[int](v)
echo repr(cast[Value](z))
echo cast[Value](z).stringify()
Don't cast ref objects unless the underlying representation is compatible (seq[byte] and strings).
Definitely don't cast ref objects to int. How would the garbage collector collect that?
Hmm... I see your point. But then how would I be able to store an object's address? (perhaps, the solution would be to use no GC whatsoever which is pretty much what I'm doing?)
Btw... here is an experimental implementation of NaN-tagging I've just written (roughly based on this article: https://nikic.github.io/2012/02/02/Pointer-magic-for-efficient-dynamic-value-representations.html). I believe it's working though I haven't had time to thoroughly test it.
Code:
import bitops
type
ObjType = enum
strObj, arrObj
Obj = ref object
case kind: ObjType:
of strObj: s: string
of arrObj: a: seq[Obj]
Val {.final,union,pure.} = object
asDouble: float64
asBits: int64
const MaxDouble = cast[int64](0xfff8000000000000)
const Int32Tag = cast[int64](0xfff9000000000000)
const PtrTag = cast[int64](0xfffa000000000000)
proc newVal(num:int64): Val {.inline.} =
Val(asBits: bitor(num,Int32Tag))
proc newVal(num: float64): Val {.inline.} =
Val(asDouble:num)
proc newVal(obj: Obj): Val {.inline.} =
Val(asBits: bitor(cast[int64](obj),PtrTag))
proc isDouble(v: Val): bool {.inline.} =
v.asBits < MaxDouble
proc isInt32(v: Val): bool {.inline.} =
bitand(v.asBits,Int32Tag) == Int32Tag
proc isObj(v: Val): bool {.inline.} =
bitand(v.asBits,PtrTag) == PtrTag
proc getDouble(v: Val): float64 {.inline.} =
v.asDouble
proc getInt32(v: Val): int32 {.inline.} =
cast[int32](bitand(v.asBits,bitnot(Int32Tag)))
proc getObj(v: Val): Obj {.inline.} =
result = cast[Obj](bitand(v.asBits,bitNot(PtrTag)))
let a = newVal(32)
echo a.getInt32()
let b = newVal(34.53)
echo b.getDouble()
let c = newVal(Obj(kind:strObj, s:"hello"))
echo c.getObj().s
Output:
32
34.53
hello
You could have tables-of-refs-to-objects, e.g. allInts:@seq[int], allFloat:@seq[float], and NaN tag the indices to those lists; it would require one more redirection to actually get the value, but would otherwise work well with any gc.
Also, subnormals (neƩ denormals) are also useful for tagging. And unlike NaNs which could be a result of a computation (e.g. 0.0/0.0) which you must differentiate from your tagged values, if you set the FPU "flush to zero" mode, they will not be generated as a result of a computation; see e.g. https://stackoverflow.com/questions/54531425/is-it-possible-in-floating-point-to-return-0-0-subtracting-two-different-values/54532647#54532647