Live data from Hacker News

Why Python Is Slow: Looking Under the Hood

jakevdp.github.io

11–20 of 156 posts

Re: Why Python Is Slow: Looking Under the Hood

#11
post #9
post #5

Why, oh, why does CPython bother keeping refcounts for small integers? Sure, it lets you make pretty graphs with [sys.getrefcount(i) for i in range(1000)]... but that's an extra memory read and write on every instruction that uses an integer. I can only imagine that not only are these extra instructions, but they're extra instructions that kill pipelining, if the interpreter needed to do "a = 1; b = 1; c = 1" for ins…

There is no such thing as "small integers" in CPython, only PyObjects, of which PyInt_Type and PyLong_Type are implementations. You can't special case integers without changing every C interface that accepts an object, and special casing every piece of code too (i.e. several hundred million lines of 3rd party extension modules, NumPy, lxml, ...). The trade-off is 1 extra machine word per integer (adding to the 2 + he…

When the GP talks about "small integers", I'm assuming he's talking about numbers between -5 and 256, which are cached ahead of time. They're never going to be garbage collected, so refcounting is largely unnecessary.

Re: Why Python Is Slow: Looking Under the Hood

#12
post #7
post #5

Why, oh, why does CPython bother keeping refcounts for small integers? Sure, it lets you make pretty graphs with [sys.getrefcount(i) for i in range(1000)]... but that's an extra memory read and write on every instruction that uses an integer. I can only imagine that not only are these extra instructions, but they're extra instructions that kill pipelining, if the interpreter needed to do "a = 1; b = 1; c = 1" for ins…

What you propose sounds like it would be a pure headache for all code which otherwise expects a uniform memory API. 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".

I hadn't considered that extensions which needed direct memory access would need access to these objects. But it's a solvable problem. The proposal might read as follows:

"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."

Re: Why Python Is Slow: Looking Under the Hood

#13
post #11
post #9

Earlier quoted context omitted.

There is no such thing as "small integers" in CPython, only PyObjects, of which PyInt_Type and PyLong_Type are implementations. You can't special case integers without changing every C interface that accepts an object, and special casing every piece of code too (i.e. several hundred million lines of 3rd party extension modules, NumPy, lxml, ...). The trade-off is 1 extra machine word per integer (adding to the 2 + he…

When the GP talks about "small integers", I'm assuming he's talking about numbers between -5 and 256, which are cached ahead of time. They're never going to be garbage collected, so refcounting is largely unnecessary.

Once again, you can't implement this without specializing every call site that invokes Py_INCREF or Py_DECREF, or in other words, "1 conditional branch per object access everywhere."

Re: Why Python Is Slow: Looking Under the Hood

#14
post #5

Why, oh, why does CPython bother keeping refcounts for small integers? Sure, it lets you make pretty graphs with [sys.getrefcount(i) for i in range(1000)]... but that's an extra memory read and write on every instruction that uses an integer. I can only imagine that not only are these extra instructions, but they're extra instructions that kill pipelining, if the interpreter needed to do "a = 1; b = 1; c = 1" for ins…

Python also keeps refcounts for other objects that shouldn't be garbage collected such as None, True, and False. The reason being to prevent having certain objects that need to be refcounted and certain objects that don't. Otherwise, C code would be littered with if statements like the below:

    if (number  256) {
      decrement_refcount(number);
    }

Re: Why Python Is Slow: Looking Under the Hood

#15

> 2. Python is interpreted rather than compiled. Can we stop saying things like this? Virtually all Python is compiled, as part of the interpretation process. There is a valid point here, of course. The point is that the top Python compilers do not compile to native code. So say that. Words have meanings. Use them correctly.

Might want to start by asking the core Python community. Check out the first sentence on wiki.python.org.

Nowadays it's the norm for interpreted languages to be JIT-compiled. They still universally call themselves interpreted languages because the jitter is an implementation detail, and the use of one does nothing to negate the characteristics they're trying to advertise when describing themselves as interpreted languages.

Think of it this way: Can I take a text file containing source code in the language and execute it anywhere that has a special program for executing files written in that language (commonly known as an interpreter)? Or do I have to manually perform an intermediate step of converting the program to some binary representation using a special program for doing that (commonly known as a compiler), and then execute the output that program produces?

Re: Why Python Is Slow: Looking Under the Hood

#17

> 2. Python is interpreted rather than compiled. Can we stop saying things like this? Virtually all Python is compiled, as part of the interpretation process. There is a valid point here, of course. The point is that the top Python compilers do not compile to native code. So say that. Words have meanings. Use them correctly.

There are three primary methods of running code: interpretation, compilation to object code, and running on a VM/JIT. Obviously python is interpreted by any meaningful definition of interpretation and thus it is not compiled in the typically sense of the word (at least not in the implementation everyone uses).

The reason human language is so expressive is because we can leave out a lot of context and formalism that is required in mathematics and programming languages because the listener/reader will be able to infer it. It's pedantic to expect a writer to hedge against every possible interpretation, when their focus should be on communicating clearly in the first place.

Re: Why Python Is Slow: Looking Under the Hood

#18
post #12
post #7

Earlier quoted context omitted.

What you propose sounds like it would be a pure headache for all code which otherwise expects a uniform memory API. 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".

I hadn't considered that extensions which needed direct memory access would need access to these objects. But it's a solvable problem. The proposal might read as follows: "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…

Of course it's solvable. My point is that solving it adds a layer of complexity. Now every decref needs a check for "id". As the id is implemented as a pointer, all of these special objects need to be allocated in a contiguous block of memory for this check to be fast, and with two extra if-conditionals. Also, this range can't change without recompiling all extensions ... or that range must be resolved dynamically, adding more overhead for every single decref.

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.

Re: Why Python Is Slow: Looking Under the Hood

#19
Pharo 3's results (JIT enabled VM)

[(1 to: 100000000) sum] timeToRun 0:00:00:07.335

http://pharo.org

Version 4 with new VM due in 2015.

Will perform much much better as with the range used, we need to use LargeIntegers as the 32 bit VM must promote to LargeInteger objects. With the 64bit VM, all fits in.

Re: Why Python Is Slow: Looking Under the Hood

#20

> 2. Python is interpreted rather than compiled. Can we stop saying things like this? Virtually all Python is compiled, as part of the interpretation process. There is a valid point here, of course. The point is that the top Python compilers do not compile to native code. So say that. Words have meanings. Use them correctly.

Might want to start by asking the core Python community. Check out the first sentence on wiki.python.org. Nowadays it's the norm for interpreted languages to be JIT-compiled. They still universally call themselves interpreted languages because the jitter is an implementation detail, and the use of one does nothing to negate the characteristics they're trying to advertise when describing themselves as interpreted lang…

There are several compiler-only Lisp implementations which can execute source from text. Nobody there would call them interpreter because of that. In Lisp we call them interpreter, when the implementation traverses the source code during execution. If it compiles the source to some byte-code or machine code, we call in Compiler.

The incremental nature of a compiler, then does not make it that we would call it an Interpreter. We would call it an incremental compiler. That's a compiler, which can compile all expression, regardless of the size of the expression, not just 'whole programs'.

Post reply on HN