[Python-Dev] Tagged integers

James Y Knight foom at fuhm.net
Wed Jul 14 08:41:19 CEST 2004

So I was saying to someone the other day "Gee, I wonder why Python 
doesn't use tagged integers, it seems like it would be a lot faster 
than allocating new objects all the time.", and they said "Cause you'd 
have to change everything, too much work!" and I said "Nah, you only 
need to change a few things to use macros, it'd only take a few hours, 
mostly query-replace".

So, of course, I had to do it then, and it only took a couple hours, 
and appears to be at least somewhat faster.

On the test that probably puts my change in the most positive light 
possible: "x = 0;  while x < 50000000:  x = x + 1", it achieves about a 
50% increase in speed. More normal integer-heavy things seem to be at 
most 20% faster.

My implementation works as follows:
- Assumes at least 32-bit words
- Lower 2 bits of machine word are the tag. tag 0 == normal object 
pointer (all object pointers are already word aligned, so their lower 2 
bits will be 0). tag 1 == integer. 2&3 unassigned.
- Integers that fit in 30 bits are stored into the word directly, not 
in allocated memory.
   - For example, integer "5" would be stored as (5 << 2) | 1 == 0x15.
- Thus, an arbitrary "PyObject *" can no longer be relied upon to 
actually be a pointer. Sometimes it will be tagged data instead.
- Thus, no code can directly access object fields ->ob_refcnt, or 
->ob_type. Introduced Py_GETTYPE/Py_GETREF macros and search&replaced 
code in python to use them. These macros check the tag bits, and do the 
right thing if it is tagged data.
- I kept tagged integers as the same class as allocated int objects, as 
it simplified implementation (nothing outside intobject knows the 
difference). It would probably be nicer to do away with heap allocated 
int objects and overflow directly to long objects after 30 bits, but, 
IIRC, there are various bits of python that require integers up to the 
platform's full integer width to fit in an instance of PyInt_Type. 
Also, subclasses of integer must be heap allocated too, of course.
- intobject.c cannot directly access ->ob_ival. Instead, I made it use 
PyInt_AS_LONG, which is modified to know about tagged integers.
- That's about it!

So, why doesn't python already use tagged integers? Surely someone's 
thought to "just do it" before? I only see discussion of it with 
relation to pypy.

A couple things:
- It's almost certainly not fully portable -- that's fine! Keep using 
heap allocated int objects on architectures that it doesn't work with.
- It does cause incompatibility with external C modules. Not only 
binary incompatibility -- the source also needs to be modified to not 
use ->ob_type/->ob_refcnt directly. ob_refcnt is hardly used outside of 
INCREF/DECREF macros already, so that's good. ob_type is used a lot in 
extension modules' PyXXX_Check macros.

Here's the patch I have against Python-2.3.3. Please note this is just 
a couple hour hack, it may have errors. Most of the diff is quite 
boring and repetitious.

So, any thoughts? Worth continuing on with this? If this is something 
that people are interested in actually doing, I could update the patch 
against latest CVS and put the changes in #ifdefs so it's compile-time 

Thoughts for future development:
   There is space available for 2 more tagged data types. Could they be 
productively used? Perhaps one for single element tuples. Perhaps one 
for single character unicode strings. Dunno if those are easily doable 
and would actually increase performance.


Here's the interesting portions of the changes. Yes, I realize typeof() 
and ({ are GCC extensions, but this was the most straightforward way to 
implement inline expression macros that may use their arguments more 
than once. Maybe they should be inline functions instead of macros?

===== object.h

#define Py_OBJ_TAG(op) (((Py_uintptr_t)(op)) & 0x03)

#define Py_GETTYPE(op) ({typeof((op)) __op = (op);  \

#define Py_GETREF(op) ({typeof((op)) __op = (op); \
      Py_OBJ_TAG(__op)?1:__op->ob_refcnt; })
#define Py_SETREF(op, val) ({typeof((op)) __op = (op); \
    if(!Py_OBJ_TAG(__op)) \
      __op->ob_refcnt = (val); \

#define Py_ADDREF(op, amt) ({typeof((op)) ___op = (op); \
    if(!Py_OBJ_TAG(___op)) \
      ___op->ob_refcnt += (amt); \
    Py_GETREF(___op); \

#define Py_INCREF(op) (                         \
         Py_ADDREF(op, 1), (void)0)

#define Py_DECREF(op)                                   \
         if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
             Py_ADDREF(op, -1) != 0)                     \
                 _Py_CHECK_REFCNT(op)                    \
         else                                            \
                 _Py_Dealloc((PyObject *)(op))

===== intobject.h

#define PyInt_AS_LONG(op) ({typeof((op)) __op = (op);  \
    Py_OBJ_TAG(__op)?(((long)__op) >> 2):((PyIntObject *)__op)->ob_ival; 

More information about the Python-Dev mailing list