Anjana Vakil - Exploring Python Bytecode

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
we are with Jon Arbuckle that is going to speak us about the Python byte code so thanks to the speaker fili we need like you know stand-up comics here to open up the crowd or something hi I'm on genomic he'll and yeah I hope you guys are excited about byte code because I am can everybody hear me okay great so uh Who am I well my name is Ann Jenna and I'm a PI the Holika in addicted to Python for probably about three years no I am right now I use Python as an outreach intern at Mozilla to do some testing work for them but what I want to talk to you about today is some explorations into the core of Python that I started doing while I was a participant at the recurse Center which is a really cool programming community in New York City where you're allowed to just follow whatever excites you about programming so today I'd like to tell you a little bit about an adventure that I had that involved getting started with Python byte code I'm by no means an expert in it but I just wanted to bring you along on my first encounters with it and show you why I think it's really cool so while I was at the recurse Center I came across this puzzle I think of it as a Python puzzle it turns out that Python code runs a faster if you stick it inside of a function and then call that function maybe you guys are already familiar with this I was not but for example if we have a rather lengthy for loop that does nothing useful it just just evaluates a variable I for each I in a rather long string of eyes if we call that just in the global Python module it takes quite a bit longer than if we stick it inside this run loop function and then call that function once and to me this was puzzling because looking at this source code I don't see any real meaningful difference in fact all I see in the inside function version on the right is that if anything python should have more work to do because it's got to create a function and then call it so i couldn't really understand from looking at the source code why this would be so much stir the right-hand-side turns out that looking at the bytecode can give us a little bit more insight than looking at the source code for certain types of Python puzzles like this one and that all has to do with what happens when we run Python code so this was something I hadn't really ever thought too much about before what happens when I actually execute a Python program and today I'm just talking about C Python a lot of this is a implementational detail specific to the C Python interpreter but hopefully that's what a lot of you guys are using and differences between C Python and other interpreters are also really fascinating but not the topic today so when we're using C Python to run some Python code we start out with our beautiful pythonic easy to read a nicely indented source code that looks fantastic and that gets compiled by part of the part of C Python that's called a compiler it gets turned into a parse tree an abstract syntax tree a control flow graph what those are doesn't really matter for our purposes right now they're all just different abstractions of what we want our code to do the important part is that ultimately gets compiled down to bytecode which obviously we'll be talking a bit more about in a moment and that bytecode whatever it is for now gets passed to the interpreter and is what the interpreter actually runs the interpreter being a virtual machine that is performing operations on a stack of objects so the interpreter executes that bytecode and then you get out whatever awesome stuff your Python program is designed to do great okay so this bytecode what is it well as we saw it goes kind of in-between it comes at an in-between place between your source code and the effects of your program so in one sense it's an intermediate representation of your program and in fact it's the representation that the interpreter itself sees the interpreter unfortunately doesn't get to look at your beautiful readable pythonic source code it only gets to see this bytecode so if we think about the interpreter as a virtual machine we could think about the bytecode as the machine code for that virtual machine so when we think of more languages that are traditionally considered compiled we think of taking source code and translating that into machine instructions for an actual physical machine in this case it's pretty much the same idea it's just that the machine is virtual and is the Python interpreter instead of the the actual physical machine and so since the virtual machine the Python interpreter that we're dealing with is basically a stack machine it's the bytecode that we give it is a series of instructions for what to do which objects to add on to that stack how to how to which operations to perform on objects that are already on it how to pop things off and return them back to us so it's a series of instructions the bytecode is and another interesting thing if you've ever wondered about those high C files that pop up all over the place when you're importing Python modules these are actually caches of the byte code this is the byte code that the compiler has already spit out and the nice thing about this caching mechanism is that since we saw that from source code to execution we have that those two steps the compilation and then the interpretation if we haven't updated the source code since the last time we ran the program we can skip the first part we can reuse the byte code that we already compiled before so that's what those pi c files are and if you've ever tried to read one of those to open one of them they're gobbledygook they're not meant for us these are easily humans to understand so how can we humans read this byte code that's intended to be read by Python well there's a really handy module called dis which has a fun name it stands for word disassembly so disassembling the byte code the documentation is is right up there put the link in there and this allows us to analyze certain types of Python objects to to read it's the the bytecode for that object in a way that we humans can understand instead of looking at the bytes themselves which isn't that helpful to us so for example if I have a really simple function that says that it's called hello and returns can somebody help me pronounce with the mask here hi Joe okay good trying anyway uh if we dis this function hello we get our first peek at disassembled bytecode cool these two white lines at the bottom here are our really really simple bytecode we just have two instructions here and without really knowing what all these numbers are what the columns are what we're looking at we can already get a sense for what's happening we're loading some kind of constant a string onto the stack and then we're returning it sweet so let's break it down what exactly are we looking at here what does it mean when we see the output of this so we have a series of rows where each row in the output is an instruction to the interpreter and on the left-hand side a lot of the time we will see a line number 2 here is the line in our source code so this is just for us to help us know what how the source code lines up with the byte code not every line in the instructions will have a line number as you can see here the return value line doesn't have one because sometimes more than one instruction can fit on one source code line so sometimes we we only see the line number when it's the instruction that starts the line and next to that we can see an offset in bytes how far into this into this string of bytes is this particular operation that's not super interesting and my perspective for us humans but what is interesting is the next thing which is this string load Const load constant source stands for and that's the name of the operation and in a minute we'll look at some more of those and see where we can find out about all the different possible operations you could encounter when you're reading this disassemble byte code if the operation in question takes arguments which not all of them do but if it does then you'll see some information about the arguments on the right-hand side so those last two columns on the right we see the argument index which interpreting that and what exactly means index in what object that depends on the operation there are a few different places that Python keeps track of the different values like constants or variable names that you would need to carry out a particular operation and that's all something you can look up in the documentation but what's more interesting for our purposes now is the value of that argument which you can see to the right and paren sieze and this is Python kind of giving you silly human a little hint about what it is that that this operation is operating on so some operations we've already seen a load constant which takes an argument C and it pushes C on to the top of the stack to us then there are things like binary ad which takes whatever is already on the top of the stack the top two items adds them together and puts that result on the top of the stack and then there's things like call function which it's argument is a bit strange it's argument tells it how many positional or keyboard arguments that function is expecting so that it knows how many objects to take off of the top of the stack and in which order to pass to that function so there's a ton of these I would not be able to cover them all even if I have an hour or more whole day but they're all conveniently documented in the documentation for the disk library the disk module so that's a linked at the top of the page here and for each of these operations the names that we see these operation names are just for us humans Python doesn't care it has a number for each of them of course that's called the opcode of the operation code and if you're curious about what the correspondence between a name and a code is for a given operation you can use these attributes this OP map and dis op name op map is a dictionary where you can just look up a particular operation name and find out its code and if you happen to already know the code you can pass it to OP name and it is an indexed list of all the the sequence of all the operations so you can find out which code corresponds to which name just some convenience there and so now we have a basic idea of how the disk function works how we can disassemble some byte code what can we use it on let's try to diss some things let's find out what we can des I love this name okay so we already saw we can discipline here's a nice little Python example one we're adding spam and eggs and if we just add we see we have a slightly ever so slightly more complex a thing to do here which is we're loading two things on spam and eggs and then we're doing a binary add on that cool starting to get comfortable with this what else we did how about a class for a really simple class here the parrot it's got a one attribute called kind it's a Norwegian blue this is money Python humor for anybody that's not familiar and it has a method is dead which always returns true and when we pass that parrot class to dis we see that it disassembles each of the methods on that class so including the the constructor method and so here we've got let's see a new new operation name here in the disassembly of dunder init here we have store attribute cool so we're starting to get familiar with some of these new operation names in my experience a lot of the times they're self-explanatory but if you're ever curious okay I don't know what that code what that operation name does just go to the disk documentation it's all laid out um another thing we can disassemble if we're using Python 3.2 or newer is a string that contains valid Python code so we don't have to actually put that code in a module we can just use it disassemble the string directly it gets compiled to a code object and then that code object gets disassembled so here we are just assigning spam and eggs on one line which is a cool thing Python lets us do and we see a new thing like unpack sequence also a pretty self-explanatory operation name okay we what about an entire module let's say I have a really simple module called night stop I it has one line it says print the string me I can actually disassemble that straight from the command line by passing the EM flag and the disk module and then just the entire contents of that night stop I cool so now we see aha we're calling this function print and we see the argument to call function is like some number of positional and keyword arguments that's what I was talking about before but what we can what we can gather from this is that we're loading on this constant and then we're calling the function print on it cool I think it's going anyway all right what about another way to dis a module well as we saw we can use code strings we can we can disco drinks so what if we read in the module using the open dot read function so now we have the whole contents of the module is a string and we can dis that cool it's basically the same thing as last time there's a little one less kind of return there but essentially we're getting the same functionality good to know and the another way we can weaken dis a module is by importing it and then dissing the imported object in this case Knights stop I got a little more complicated we added this method is flesh wound or a function is flesh wound which always returns true and as you'll notice when I import Knights the whole modules getting executed it prints me but in the in the disassembled bytecode we don't see any mention of the printing part all we see is is flesh wound so when you do it this way when you try to dis a module this way by importing it it's only going to disassemble the functions in that module anything else that's there just kind of as a script is going to get it's not going to get put in the output of disk so that's just something to know about the different ways of using this okay there anything else we can dis how about nothing what have we passed no arguments to it in this case we're not dissing nothing we're dissing the last trace back the last ever which is a cool thing because let's say I tried to print this variable spam which I had forgotten to assign so I get this name error of course if I do dis dot dis with no arguments I can see the bytecode that tells me exactly where that error came from so you see the arrow to the left of the operation names there that indicates that okay when I loaded print that was fine I found print okay but when I loaded spam I had a problem so these are some different things that we can dis which if you're like me is just fun to just spend lots of time just dissing everything you can get your hands on just to see what they do and apparently can also help you in solving some puzzle challenges that one of the sponsors has out there but other than that why do we care about doing this why do we want to do this if we're not at a conference where we get free USB power backs if we solve puzzles well as we saw when we use the discus with no arguments that's a really useful debugging tool because time's the error messages that we get from Python although they're usually wonderful sometimes they don't tell us everything we need to know so for example let's say let's say I had a line in a really complicated mathematical code there that that is dividing two has two division operations on the same line so Hamed divided by X plus hand divided by spam that gives me a zero division error and it tells me what lying in my code the zero division error came from but it doesn't tell me whether it was eggs or it was spam that gave me the error if I diss the trace back I can actually see that okay we were going to reloaded ham we loaded eggs we did a true divide and there was no problem ah okay so eggs was fine then we loaded ham again we loaded spam and then when we did that derivative I'd that's that little arrow says that's where the problem was so I know that the problem in my complex mathematical computations is spam and that's what I have to go back and fix so this can be a really cool debugging tool for certain situations and it can also be a helpful tool to solve puzzles not just the kind that the sponsor has but also the kind that I mentioned at the beginning where we have this for loop which takes a lot longer outside of a function than in and yet in the source code it looks pretty much identical so let's try and get a little bit more insight here by dissing this outside function module and the run loop function from the inside function and module and see how they compare okay so we have outside function pi now we know a few different ways of dissing a module I'm going to choose the the reading the open read method get a string called outside and then just that so this is now what Python sees when we run that outside function pipe okay I don't understand all of this I don't necessarily need to I can get a general sense of what's going on we're loading this range function we've got a really somewhat big number that we're loading in then we have ah this new thing get it and for it ER for it er that's our that's our for loop there so that's what that looks like to Python cool and then inside of that where we're storing I I guess for each time we go through the for loop but then we're loading I because we had a really really useful for loop in that code that we just saw and okay all right seems to make somewhat sense let's see how it compares with inside so from from the inside function PI file what we care about is this run loop function so I'm going to import that in I'm going to call it inside just for convenience and symmetry and then I'm going to disc inside at first glance this looks pretty much the same as what we just saw so let's see if switching back and forth really fast will tell us anything outside inside outside inside okay okay so what do we notice differences well first of all on the left hand side we notice that some of the line numbers are different that's because we had one extra line in the in the inside function we had that function definition that's probably not important what else we got aha with the range function in one case it's load oops it's loading as a name and one case it's loading as a global alright maybe there's some difference there but we're only doing that once opes that's probably not that big of a deal what we probably care more about is what happens inside the iteration so after that for itter and here we see okay when we're doing inside we're using something called store fast and load fast and we're doing outside its store name and load name C 16 and 19 there so I don't know what those mean store fast sounds like it would be faster and load fast sounds like it would be faster but I don't know why or what these do so how can I find out well it can investigate by going into the disks documentation where it has a list of all of the different operation codes and tells you what they do I've just copied those over here okay store nand soon and I said okay code names I don't know that is all right load name is using code names again okay so it looks like store name has something to do it has to look up something with an index and then it goes finds the attribute and so maybe that's something that could be possibly slowing us down whereas store fast and load fast they're using something else called Co var names and we don't see anything about looking up indices and whatever so that might have something to do with it this is starting to get me on the right path and if you're really interested in digging in if if the disk documentation hasn't answered all of your questions you can go right to the beating heart of Python and dig deeper in to see eval dot C which is where the Python interpreter a process is all of these different codes and there's a really cool talk by Allison Kaptur called a 1500 line switch statement power to your Python this is true there's a huge switch statement where it's it's telling c python what to do with all the different operation codes that you might encounter and so if we look at the the actual code for those operations load fast and load name we see that load fast is like little bitty thing it's like ten lines and it involves a lookup into an array called fast locals which sounds fast because it is fast load name on the other hand first of all it's more code it's longer it's more complicated it's about 50 lines and it involves a dictionary lookup which is quite a bit slower so it turns out that one of the main speed differences here which is a little bit tangential to the bytecode discussion is that when you have a code inside of a function because when you just define the function you know how many variables you need in that function Python can just assign a fixed length array so when it needs to look up something in that function it can just index into that array and pull it out really quickly whereas when you have it in the global scope it doesn't know you might you might assign new variables all the time so it keeps things in a dictionary and so looking up from that dictionary is a bit slower anyway then there's another thing called opcode prediction which makes it even faster if you combine certain operations together because cpython can predict what's coming next and it has an idea it can save some some ticks by by doing common operations that always go together by predicting it in advance and so the combination for it err and store fast happens to be one of these predicted combinations it moves a lot faster than combining for it er and store name so if you're curious I saw so strongly suggest you check out this really cool Stack Overflow conversation why does Python code run faster in a function and Alison captors talk switch talk a bit more about how we can start exploring this giant switch statement that tells Python how to interpret all of these different operation code
Info
Channel: EuroPython Conference
Views: 17,580
Rating: 4.9749479 out of 5
Keywords: CPython, Compiler and Interpreters, EuroPython2016
Id: GNPKBICTF2w
Channel Id: undefined
Length: 22min 37sec (1357 seconds)
Published: Fri Aug 05 2016
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.