Wandering Thoughts archives

2017-12-29

To get much faster, an implementation of Python must do less work

Python, like many other dynamically typed languages, is both flexible and what I'll call mutable. Python's dynamic typing and the power it gives you over objects means that apparently simple actions can unpredictably do complex things.

As an example of what I mean by this, consider the following code:

def afunc(dct, strct, thing2):
  loc = dct["athing"] + thing2
  return loc + strct.attr

It's possible and perhaps even very likely that this Python code does something very straightforward, where dct is a plain dictionary, strct is a plain object with straightforward instance attributes, and all the values are basic built-in Python types (ideally the same type, such as integers) with straightforward definitions of addition. But it's also possible that dct and strct are objects with complex behavior, dct["athing"] winds up returning another complex object with custom addition behavior, and thing2 is another complex object with its own behavior that will come into effect when the 'addition' starts happening. In addition, all of this can change over time; afunc() can be called with different sorts of arguments, and even for the same arguments, their behavior can be mutated between calls and even during a call.

A straightforward implementation of Python is going to go through checking for all of these possibilities every time through, and it's going to generate real Python objects for everything, probably including intermediate forms and values. Even when strct is really just a plain object that has some fields but no methods or other behavior, a Python implementation is probably going to use a generic, broad implementation (even __slots__ is fairly general here; a lot of things still happen to look up slot values). All of this is work, and all of this work takes time. Even if some of this work is done inefficiently today in any particular implementation, there is a limit to how much it can be improved and sped up.

This leads to a straightforward conclusion: to really get faster, a Python implementation must do less work. It must recognize cases where all of this flexibility and mutability is not being used and then skip it for more minimal implementations that do less.

The ultimate version of this is Python recognizing, for example, when it is only working with plain integers in code like this:

def func2(upto, alist):
  mpos = -1
  for i in range(upto):
     if (alist[i] % upto) == 0:
        mpos = i
  return mpos

If upto and alist have suitable values, this can turn into pretty fast code. But it can become fast code only when Python can do almost none of the work that it normally would; no iterator created by range() and then traversed by the for loop, no Python integer objects created for i and the % operation, no complex lookup procedure for alist (just a memory dereference at an offset, with the bounds of alist checked once), the % operation being the integer modulo operation, and so on. The most efficient possible implementations of all of those general operations cannot come close to the performance of not doing them at all.

(This is true of more or less all dynamic languages. Implementation tweaks can speed them up to some degree, but to get significant speed improvements they must do less work. In JIT environments, this often goes by the term 'specialization'.)

FasterPythonMustDoLess written at 01:37:56; Add Comment

2017-12-14

How Python makes it hard to write well structured little utilities

I'll start with the tweets, where I sort of hijacked something glyph said with my own grump:

@glyph: Reminder: even ridiculous genius galaxy brain distributed systems space alien scientists can't figure out how to make and ship a fucking basic python executable. Not only do we need to make this easy we need an AGGRESSIVE marketing push once actually viable usable tools exist. <link>

@thatcks: As a sysadmin, it’s a subtle disincentive to writing well structured Python utilities. The moment I split my code into modules, my life gets more annoying.

The zen of Python strongly encourages using namespaces, for good reasons. There's a number of sources of namespaces (classes, for example), but (Python) modules are one big one. Modules are especially useful in their natural state because they also split up your code between multiple files, leaving each file smaller, generally more self-contained, and hopefully simpler. With an 'everything in one file' collection of code, it's a little too easy to have it turn mushy and fuzzy on you, even if in theory it has classes and so on.

This works fine for reasonable sized projects, like Django web apps, where you almost certainly have a multi-stage deployment process and multiple artifacts involved anyway (this is certainly the case for our one Django app). But speaking from personal experience, it rapidly gets awkward if you're a sysadmin writing small utility programs. The canonical ideal running form of a small utility program is a single self-contained artifact that will operate from any directory; if you need it somewhere, you copy the one file and you're done.

(The 'can be put anywhere' issue is important in practice, and if you use modules Python can make it annoying because of the search path issue.)

One part of this awkwardness is my long standing reluctance to use third-party modules. When I've sometimes given in on that, it's been for modules that were already packaged for the only OS where I intended to use the program, and the program only ever made sense to run on a few machines.

But another part of it is that I basically don't modularize the code I write for my modest utilities, even when it might make sense to break it up into separate little chunks. This came into clear view for me recently when I wound up writing the same program in Python and then Go (for local reasons). The Python version is my typical all in one file small utility program, but the Go version wound up split into seven little files, which I think made each small concern easier for me to follow even if there's more Go code in total.

(With that said, the Go experience here has significant warts too. My code may be split into multiple files, but it's all in the same Go package and thus the same namespace, and there's cross-contamination between those files.)

I would like to modularize my Python code here; I think the result would be better structured and it would force me to be more disciplined about cross-dependencies between bits of the code that really should be logically separate. But the bureaucracy required to push the result out to everywhere we need it (or where I might someday want it) means that I don't seriously consider it until my programs get moderately substantial.

I've vaguely considered using zip archives, but for me it's a bridge too far. It's not just that this requires a 'compilation' step (and seems likely to slow down startup even more, when it's already too slow). It's also that, for me, packing a Python program up in a binary format loses some of the important sysadmin benefits of using Python. You can't just look at a zip-packaged Python program to scan how it works, look for configuration variables, read the comment that tells you where the master copy is, or read documentation at its start; you have to unpack your artifact first. A zip archive packed Python utility is less of a shell script and more of a compiled binary.

(It also seems likely that packing our Python utility programs up in zip files would irritate my co-workers more than just throwing all the code into one plain-text file. Code in a single file is just potentially messy and less clear (and I can try to mitigate that); a zip archive is literally unreadable as is no matter what I do.)

UtilityModularityProblem written at 17:45:09; Add Comment


Page tools: See As Normal.
Search:
Login: Password:
Atom Syndication: Recent Pages, Recent Comments.

This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.