This isn't so bad on memory either, since Python keeps a static cache of integers that are reused on every int constructor call. Also allowing for slack, I wouldn't be surprised if the machine word came for free.
This is essentially why a JIT can be almost as fast as compiled code even when including the necessary guards. It's only when the branches become unpredictable that the costs become real. The trick with fast interpreted code is threading the execution so that the hardware is able to predict the branches accurately, but this is largely independent of the number of conditional branches.
Consider a C extension which takes an object and appends it to a list. If small integers did not have a refcount then that extension would have to have special code, like "if object is not a small integer, then increment the reference count".
"Objects with IDs between 0 and 2055, inclusive, are considered unreleasable. The Python runtime shall initialize the refcount of these objects to 1 when they are first used, and shall make no guarantee that the refcount on such objects accurately reflects the true number of references to that object. Native extensions may increase and decrease the refcount on these objects as if they were normal objects; if those extensions are well-behaved, then the refcount should never decrease below 1. However, native extensions that can statically reason about whether an object is likely to be unreleasable, may benefit in performance by checking for whether objects passed to their functions are indeed unreleasable, and refraining from modifying or checking their refcount in such cases."
Nor is there much advantage. We know that more advanced implementations can look at program flow and determine that, say, certain operations are only integer based, and therefore unboxable. Or tracing systems can assume that certain types are constant over multiple calls to the same code, and write optimized versions for those types.
These gives much better (eg, >5x according PyPy) optimizations over the error prone change that you believe might be faster.
> If small integers did not have a refcount then that extension would have to have special code, like "if object is not a small integer, then increment the reference count".
Easily implemented in one place in the "increment_refcount(obj)" inline function:
// roughly speaking
if (is_heap_pointer(obj))
obj->refcount++;
where "is_heap_pointer(obj)" is an inlined bitmask check like (((unsigned int) obj) & TAG_MASK) == TAG_PTR)
If TAG_PTR is zero bits, then a value which satisifes is_heap_pointer can be dereferenced straight.In a garbage collection implementation, you don't have refcounts, but the garbage collector's "mark object" function does the same check:
if (!is_heap_pointer(obj))
return; // don't try to mark non-heap things
switch (type(obj)) {
case TYPE_CONS_CELL
mark_obj(obj->cons.car);
mark_obj(obj->cons.cdr);
break;
// ...
}
Another thing is that you provide an API to the extension writers, which abstracts the use of objects. For instance, you can give them a function that can be called like this: value n = number(42);
value z = number(INT_MAX);
The first case might construct an unboxed value because the integer is small enough. But perhaps the second returns a bignum because INT_MAX requires 32 bits, wheres unboxed integers only go up to 30 bits.So in the first case you get an object with no refcount, whereas in the second you get an object with a refcount of 1. The extension code is written such that it doesn't care.
See https://mail.python.org/pipermail/python-dev/2004-July/04614... .
I just realized though that since the patch to support tagged integers is small, a closed system like the CCP Games distribution of Python, which has no extensions they don't control, might be able to use this idea.
if (number < -5 || number > 256) {
decrement_refcount(number);
}