
Memory use in CPython and MicroPython - signa11
https://lwn.net/Articles/725508/
======
raymondh
My least favorite part of Python 3 is the new 28 byte int structure which
spills over to 32 bytes at 1,073,741,824 (2^30).

    
    
        >>> sys.getsizeof(1073741823)
        28
        >>> sys.getsizeof(1073741824)
        32
    

As a directly result of the complicated implementation of Python 3 ints, we
lost some important optimizations.

In Python/ceval.c, BINARY_SUBSCR can no longer easily have a fast path for
tuple and list indexing with integers. This makes somelist[0] about as slow as
somedict[0]:

    
    
        timeit36 -s 's=[0]' 's[0]'   # 0.0320 usec
        timeit36 -s 's={0:0}' 's[0]' # 0.0331 usec
    

Likewise, the builtin sum() function lost speed on its fast path for summing
integers. That path is still there but it now uses the slow
PyLong_AsLongAndOverflow() function instead of the PyInt_AS_LONG() fast macro.

    
    
        $ timeit27 'sum(xrange(1000000))'  #  7.32 msec
        $ timeit36 'sum(range(1000000))'   # 19.20 msec 
    

IMO, Python 3 int/long unification should have just dropped the trailing "L"
from the display of long integers and called it done.

From the user's point of view, that would have unified the two types enough to
mostly not care. You would still have two different types for int and long,
but otherwise the switch back and forth was already somewhat seamless in
Python 2.7.

The range of single cell ints was enormous (up to 9,223,372,036,854,775,807 or
2^63-1). It fulfilled typical use cases without spilling into slower and more
complex multi-cell calculations and without impeding useful downstream
optimizations for common cases.

    
    
        >>> sys.maxint
        9223372036854775807
        >>> sys.getsizeof(sys.maxint)
        24
        >>> sys.getsizeof(sys.maxint + 1)
        36
    

Once Python 3 was released and the API guaranteed that all integers were the
same type regardless of size, that design choice was set in stone. So, now we
will have to live with it forever :-(

~~~
ceronman
Python's int/long unification was done in Python 2.2 (PEP 237). Which by the
way, I think it's a great feature, even if it makes it slower. That is what
Python is about after all: simplicity and ease of use over performance. If you
really need the max performance possible, don't use Python.

As far as I knew, the only thing that changed in Python 3 was that they
renamed the long type to int, and they also removed L suffix. But from your
code seems that other things have changed as well and now two structures are
used for short integers. Do you know when this happened exactly and why?

~~~
saurik
> the only thing that changed in Python 3 was that they renamed the long type
> to int

Python 2.x had two types: int and long. Transitions from int to long were
pretty seamless and the two types were somewhat compatible. This is not
unification: there were still two separate types.

Python 3 has one type called int that functions mostly like long. You can sort
of say (I guess) that "long was renamed to int", but only if you say "and int
was removed": really, the types were unified.

------
netheril96
When Python 3 broke compatibility, it would be perfect timing to implement
tagged pointers, small integers and other performance improvements. At least
it would be an easier sell to your boss to rewrite your code base. "We are
doing so it runs faster" is much better than "we are doing it for intellectual
purity" from a business standpoint.

~~~
bramblerose
Why would "makes your code run 1% faster" be a better sell than "saves you 1%
development time by reducing the number of bugs"?

~~~
egwor
That tends to be a more difficult sell because the counter-argument from
further up is 'But we're not seeing those bugs right now, and won't we find
bugs when we do this upgrade?'

------
dom0
The comparison is interesting, but CPython's lavish use of memory (which of
course has a performance impact by clogging caches, though that's one of the
least problems you have with CPython, performance wise) is well known. ¹

This is in part because there are still macros such as PyBytes_GET_SIZE which
directly access struct members, and these macros are part of the stable
interpreter ABI. That doesn't mean small integer opts and such for length
fields aren't possible, it just means they can't happen for Python 3 any more.
Tagged pointer would probably break too much code to ever happen.

¹ well known as it may be, people are still surprised that bytes() requires at
least 33 bytes (due to the implicit extra NUL byte), an empty string is around
50 bytes and every item in a dict or set takes between 30 and 70 bytes. All
this overhead adds up.

\--

Borg works around these problems with a simple hash table (straight out of the
text book, with some associated issues). Even though that one is in itself
inefficient in how it uses memory, it still only uses a fraction of what the
equivalent dict in CPython 3.6 would use. I recently added a similar, pure
Python construct in another place (borg mount).

------
manaskarekar
Off topic: Micropython on ESP8266 is a blast.

1\. Pick yourself up a < $5 ESP8266 board from ebay (Wemos D1 mini or NodeMCU
Lolin).

2\. On debian based linux:

\- Get the correct binary from
[http://micropython.org/download](http://micropython.org/download)

\- sudo pip install esptool

\- esptool.py --port /dev/ttyUSB0 erase_flash

\- esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash
--flash_size=detect 0 /path/to/binary.bin

3\. Hack away!

------
bastawhiz
MicroPython is cool, but the incompatibility with CPython's MRO is a deal
breaker for me (please correct me if it's been changed). My understanding is
that ancestor methods on classes using multiple inheritance are not called in
the same as in CPython. This seems like a minefield, especially in codebases
using lots of mixins.

