字幕列表 影片播放 列印英文字幕 (relaxed music) - So we're gonna be talking about memory layout in Swift. As I'm sure you know, Swift is Apple's brand new, magical, fancy programming language and I'm gonna go dive into it a little bit and talk about the bits and bytes and how it's all put together and what stuff looks like in memory when you actually run the code on a computer. Real brief about me, I'm online at mikeash.com, I have a blog where I do all sorts of crazy stuff like this, I like to take things apart and see how they tick and I've got a bunch of crazy Github projects which you should probably never use for anything, but are lots of fun to play with. I'm on Twitter if you feel like following me. There's a picture of my cat because, you know, the internet is all about cats and we're fundamentally all about the internet these days. I fly gliders, just point of information, it's a lot of fun. And the arrow is kinda pointing over here so that's me, you know, I always put a photo of myself on these slides and then afterwards I'm like, people can just look at me. So I decided I'd stop doing that. So here's the plan; first I just wanna give a quick overview of what memory is. I'm sure you all know what memory is, but it can help to get a little bit of perspective and just tear it down, you know, go to the foundations, revisit the fundamental stuff. Then I wrote a program that basically is where I generated all this information, it actually goes through, crawls, the program starting from a particular value and dumps everything out that are in those values in memory. And then finally I'm gonna actually dive into how Swift lays out stuff in memory, what that program actually produces and some contrast with how C does it and how C++ does it. So what is memory? And fundamentally memory is what stops this from happening. So you gotta keep track of where you are essentially. You've got a computational process and you are at some state within that process at all times. And if you can't keep track of that then you will just never get anywhere. So we don't wanna just endlessly repeat, we wanna actually make progress and that's what this is all about. So figuring out how to actually build hardware which can remember things and store information and dig it out later is kinda one of the fundamental problems in computing and there's lots of technologies along the way. Started out with vacuum tubes. You can imagine these things are like this big and they're essentially like an incandescent light bulb and each one holds one bit. So if you wanna actually store some reasonable amount of data you're talking about a room full of incredibly hot equipment. Later on there were mercury delay lines, this is kind of a cool technology of a pipe, you basically fill it with mercury, you have a speaker or something like that on one end and something like a microphone on the other end and you pulse your data through it. And it takes time to travel and because of that you can fit stuff in and store your information that way. And there was a fun little proposal, somebody decided that gin would make a good medium for this, had all the right chemical properties and whatever. As far as I can tell nobody ever built that, but fun little aside. Magnetic core memory was an advancement of this stuff, it was a very neat technology, you got little rings of iron and you run wires through them and depending on the electrical current you send through them you can store data or retrieve data by storing it in the magnetic field in those rings. And so that was one ring per bit. And the state of the art of this in the 60s or 70s was basically a cube about this big could hold 32,000 bits of information and then you can imagine this thing I've got in my hand can hold many gigabytes memory, billions of bits. And so things have advanced a lot since then. So DRAM, dynamic RAM, basically silicon chips is the state of the art today. Which we should all be incredibly thankful for because it really makes our lives a lot easier. That fact that we can have these, this allows us to store billion and billions of bits of information all at once. And my phone is misbehaving, if you can pardon me for just a moment here, there we go. Alright, so that's the hardware view of things, we don't really care too much about hardware most of the time if we're programming because that all just works, we ignore it. So how does it look for a programmer? So we've got the fundamental unit of information is the bit, that's a one or a zero. Traditionally we organize bits in groups of eight, those are bytes, and memory is essentially just a long sequence of bytes one after the other just heading off into the mist. And they're arranged in a certain order, every byte gets a number, that's called it's address. So we start at zero and then one and then two and then three and then an 8,000,000,000 byte system we've got billions off in the distance. It can be, you can view these things in different directions, often we view it like this, organized by word instead of by byte. So a word is a vague term of the art in computer science, but usually it means a unit that's the size of a pointer. So on modern devices it's 64 bits or eight bytes. And it just heads off into infinity. So here I've got the bytes addressed by eight. And we like hexadecimal. Hexadecimal is where you've got base 16 addressing instead of base eight. So zero through nine then A through F, that's a nice multiple of two so everything fits together nicely, it's kind of the native language of computing. So it's the natural language to use here. And so I've got the addresses done in hexadecimal instead, zero, eight, 16 is 10, 24 and all that. And this is just the big picture of what this whole thing looks like. If you zoomed out this is a Mac running on x86-64, everything's a little bit different and it's all very platform specific, but essentially you've got a gap of stuff that doesn't exist, the first four gigabytes of memory is not mapped, this doesn't take up any physical space it's just an addressing trick. Then you've got the user, your program essentially, your memory is the green stuff. So you get a chunk that's for you and then you've got a nice, big, empty space after that and then finally the kernel is down at the bottom. So you've got this two to the 64th power bytes which get sliced up and organized like this. And this is essentially how it looks if you zoom in a little bit, so this is the same picture as before except it's more realistic because instead of starting at zero we're starting at 4,000,000,000. We've got pointers in memory, I'm sure you're all familiar with the term pointer, references. A pointer at this level is just a number. And it's a number that just happens to correspond to the address of something else in memory. So here we've got this thing up there, that stores the address of this bit down there and I just indicate that with an arrow. The arrow doesn't exist in reality, it's just a number that we treat as if it were an arrow. And one more detail on all of this, whoops, went too far, we in the most modern systems store things in little-endian order which is essentially the least significant part of the number comes first. So it's as if you wrote 234 as 432, everything's backwards, just one of those things you just kinda have to learn to live with it and read it that way, so. Memory is organized into, as far as we see it, is organized into three fundamental parts. We've got at the hardware level it's just a big list of stuff. But the way we actually treat it, parts of it have more specific purposes. So we've got the stack which is where all your local variables go when you write variables that you're using in your computations in a function, that goes on the stack and it's called a stack because every time you make a function call it adds that function's local variables to it. And when you make another call it goes up and another call it goes up and then when you return from a function it goes back down, back down, back down. So it's like a stack of things. You've also got global data in your program. Those are essentially things that came along as part of the program when you loaded it. So your global variables are part of that, your string constants, your type metadata in Swift, in other languages, just gets loaded as part of that. And then you've got the heap, and the heap is dynamically allocated stuff, when you create a new object, that allocates some memory that's on the heap. And these are things where basically they don't live permanently, they've got some lifetime but they're not tied to a function, they're not tied to your program, they come to life and go away kind of arbitrarily. And that's essentially everything else. When you create a new object, when you allocate memory manually, when you concatenate strings, whatever, that's all on the heap. So that's kinda the overview, remind you what memory is, what the whole deal is that we're talking about here. Let's get into dumping memory. Actually diving in and inspecting the contents, seeing what's actually in there. So I'm gonna explore this program that I wrote, how it works, that actually goes in and traces all this stuff out. If anyone wants to take a look at it I've got it up on Github, the Github address is there, or there's a tiny URL below, or if anyone likes really huge URLs you can use that one at the bottom, but I'm not gonna wait for you to type it out. Just real quick, this uses Xcode 8 and Swift 3. If anyone's doing anything with Swift so far you know that there have been a lot of versions of it and they like to break compatibility. So last year was Swift 2, starting a couple of weeks ago we've got Swift 3 now and that's different. So if you wanna use that code you need this. So back to this. The kind of fundamental unit of this program is a function that looks like this. And this is a function that works on an arbitrary type, it takes a value and it's gonna return an array of unsigned eight bit integers, or bytes. And we'll just use it as in the demo above, you create a variable containing something, any arbitrary thing, and then you just call this and it'll hand you back the bytes that make it up. And that's going to serve as the foundation for this whole program. And the question is, how do we write this in Swift specifically? This is a real quick overview of one possible implementation which is not what the program actually uses, but it kinda gets the ball rolling as far as how this works. So the idea is you get the value, you get it's size and because we know the type of the value, this is a generic function in Swift so it works on any type, but it knows what type it's being called with at any given time. This memory layout type allows us to get the actual size, so that tells us how many bytes it is, so we know how long it is in memory. And then there's this built-in function in Swift called with unsafe pointer. So you call that, you give it a variable and it comes back and gives you a pointer to that variable. And once we have a pointer we can do things like look at that pointer as if it were a pointer of a different type. So imagine you have a pointer to an int and we do this with memory rebound call here which says, okay, pretend that this is not a pointer to an integer, pretend this is a pointer to bytes, and just work with me on this, it's the same thing but it's a different type. Now go through and read it. So what this does, this takes a pointer to whatever arbitrary thing you've got and says, alright, just pretend it's raw bytes, interpret it that way. And then once we have that we can go through and just read one by one and that's a bit of a shortcut here, I just tell the system to read it for me, there's no loop or anything like that. Unsafe buffer pointer basically lets me say it's a container and then I can create an array from that and it all just kind of happens. Swift let's you write short stuff like that. So real quick demo of what this produces. I created a variable that contains one, two, three, four, five, six, seven, eight and then I just dumped out 42, and if I print these things then I get these results here. So you can see the first one prints out exactly what we saw except it's backwards because remember modern systems tend to do things backwards. So it prints out eight, seven, six, five, four, three, two, one, zero and then 42 comes out as 42 with a bunch of zeros after it because it's a 64 bit integer. And just real quick, you don't need to actually follow this, this is just some code I wrote that I wanted to put up real quick. Hexadecimal is again the natural language of low level computing. So this just takes an array of bytes and dumps it out as a hexadecimal string instead of as a sequence of decimal integers like we saw before. So if we use that then we get this instead. Same basic thing except instead of printing decimal we get hex. So one, two, three, four, five, six, seven, eight comes out just as it did before and then 42 comes out as 2a, since that's what 42 is in hexadecimal, followed by a bunch of zeros. So this let's us dump this stuff out in a form that we can understand, but that's still close to what the computer has. Alright, so if you got this where we can take a value and see what's in it, but real programs are more complicated than single values, right, they've got a lot more going on. Real programs look more like this. Okay, you've got a value which contains a bunch of bytes and some of those bytes are actually pointers which point to other stuff and those point to other things and you get this whole tree thing going on. So we want to be able to actually chase all this stuff down in an automated fashion. The program needs to be able to actually find all of this stuff. So how do we do that? We start off with the knowledge that pointers are just integers, alright, a pointer is just an address, it's just a number which we interpret as another location in memory. So I wrote a quick struct which gets used in the program, all it is is it just contains an address which is an unsigned integer, pointer sized integer. And wrapping it in a struct helps me keep things apart so you don't confuse which parameters are actually integers and which parameters are integers which we are treating as pointers, right. Just make a new type so that the type system helps us write program correctly. And then this bit of code essentially takes an array of bytes, which we already know how to obtain, we just wrote that function, and it takes that array of bytes and tries to scan for pointers in it. So again, a pointer is just an integer that you happen to treat as an address. And we can't know how stuff is being treated at this level because we just get a bunch of bytes and we don't know what they mean. So we're just gonna kind of optimistically go through and slice it up into chunks of eight bytes and pull them all out and pretend, say, what if these were pointers, what would that mean? So that's what this does, we take this array of bytes and we say, instead of treating it as an array of bytes gives me a pointer to it's contents and treat it as a pointer to pointers, okay, which means that we can essentially go through and say, read the first pointer size chunk, read the second pointer size chunk, read the third pointer size chunk. And then we take all of that and return it as an array. So this code essentially will just go through this big array of bytes that we get from the thing before and divide it up, which, like this. So this is a visual indication of what's going on with that code. So we give it a value, it returns a bunch of bytes then we go through, slice it up and get the individual pieces. And then we can start chasing those down. So we can read a value, grab all of it's bytes, then we can grab all of the pointers that those bytes might indicate. And then we can take those pointers and repeat the process and essentially that gives you the tree, you can in a loop keep going through as long as you've got pointers to explore, you read their contents and then you spit them out. The problem with this approach, we don't know which pointers are actually pointers and which pointers are just integers, it might be the player's high score, it might be the number of people who dislike you or something like that and we don't know what they mean. And normally in a program when you try to read from a pointer that's not actually a pointer, it just is some illegal piece of memory, then your program crashes which is good in normal code because you don't want to proceed when your program is that confused, you want it to just stop and produce a crash log or something like that. But in this code we want to be able to keep going so we can explore this stuff. So we wanna be able to read from pointers without crashing if they're bad. So on the Mac and on iOS we've got this nice low level function, Apple platforms use a mach kernel highly modified and added onto stuff, but the low level mach calls are still there and there's a mach call called mach vm read overwrite. And essentially it's a system call where you give it two pointers and you say, I wanna copy this many bytes from that pointer to this pointer. If you're familiar with the memcpy function from the C standard library, it's exactly like that except that if you give memcpy a bad pointer your program crashes and if you give mach vm read overwrite a bad pointer it's okay, it returns an error because it's a system call, it happens at the kernel level, the kernel level can do all this checking safely and so it can come back and say, I couldn't do that because that is not a real pointer, that was just a bunch of junk and the address there doesn't exist. And so based on that we can go through and reliably follow this tree without crashing because we can essentially optimistically try every pointer, pass it to this function and then if it comes back and says there was an error, that's fine, we just say, okay, couldn't follow that, keep on going. This is a real quick, this is just a function prototype what looks like it takes a task, which is like a process, if you've got the right permissions on the Mac you can actually read from other processes not your own which is sort of the foundations of how you can build a debugger. It takes an address, it takes a length, it takes a destination address and it takes a pointer to something where it will tell you how many bytes it actually read. So back at the beginning I showed a function that would read from a pointer, but it would crash if you gave it a bad pointer, this will read from a pointer safely. So essentially it's just a wrapper around that mach call. It takes the pointer you give it, it does a little bit of casting to get it into the form that the system wants and then it just makes that call. If it succeeds it returns and says, hey, we did it, and if it doesn't then it returns false, the caller can know that it didn't work. And so that way based on this we can build this whole recursive scanning system. Let's see, there we go, alright. So we can read this stuff safely, but we need to know how much to read. The first value we read we can get the size of the type because we know the type it compile time, it's a generic function, we get that metadata from the compiler, but after we start chasing pointers we can't do that anymore because we're dealing with arbitrary bags of bytes, we don't know what this stuff is so we need to know, we need to be able to at least guess how many bytes to read at any given time, when you chase these pointers through. For stuff that's on the heap, there's the malloc size function, at least on the Mac and on iOS, where you give it a pointer and it comes back and says, there were actually 32 bytes allocated on the heap here. So we can call that and it comes back and tells us exactly how much we can read. Which is great. And even better, this function is tolerant of bad data, so if you give it something that's not a pointer or you give it a pointer to something that's legitimate, but not allocated on the heap, or you give it a pointer to something in the middle of something else, whatever, it doesn't care, it'll give you back zero. So it doesn't crash, which is really convenient for our purposes. And finally, we've got global variables, code, things like that. Those are symbols in your app, there's the dladder function where you give it an address and it comes back and tells you what symbol is nearby. And so we can use that to check to see if something is actually a symbol and we can also use it to kind of extract the size by essentially scanning. It gives you the symbol that comes immediately before the pointer you give it. So you start from here and you say, give me the symbol information and if it comes back and says, yes, I have symbol information, then advance it by one byte and say, how 'bout here, how 'bout here, how 'bout here, and just keep doing that until it gives you a different answer and then you know exactly how long that thing was. And as a bonus, it also gives you the names. So your function names, your global variable names, things like that, those all pop out of this API, and so we can use them to annotate our scan and help us understand what's going on. Those names in Swift and also in C++ tend to come up mangled because the compiler tries to embed information about what the type is besides just the name. So in C for example, if you have a function called summon, the symbol name that it spits out just says summon, and in Swift if you have a function called summon the symbol name comes out more like this where you've got a bunch of extra stuff on it because it will not only include that name, but it will also include the fact that it takes two integers and returns a string or whatever it actually is. So in order to help with that there's the Swift demangle command that comes with Xcode. I imagine it's available in the Swift open source tools as well. You give it a mangled symbol and it comes back with something like this which is more readable. So in my code I just dump everything through that. Swift demangle is a very nice program because if you give it something it doesn't understand it just gives it back to you unmodified. So I could just feed everything through it without having to fear that it would explode or crash or something like that on data that wasn't actually mangles Swift symbols. And then C++ has the same thing, there's a tool called C++filt which does the same job for C++ names and it has the same semantics where if you give it something it doesn't understand it gives it back to you without changing it. So I could just pass every name that I came across to these two tools. A lot of the data that we come across in memory is actually strings, alright, textual information like method names, like user input, and it's useful to be able to find these. And the trouble is again, we're working with these bags of bytes, we don't know what's going on with them, they're just a sequence of data and we want to be able to at least guess at which sequences of data actually represent text and which don't. And there's no way to do this reliably, but a decent heuristic is to look for ASCII characters and look for printable ASCII characters, so zero through 31 in ASCII are control characters which we don't expect to find as part of text in a program, at least not the text that we're interested in. And then stuff beyond 126 is either the delete character in ASCII, or it's non-ASCII characters. So we look for printable ASCII characters and we look for sequences of at least four. So the idea is that if you just have one or two or three then it's likely that's just some other data that just coincidentally happened to look like text. And once you get up to four there's a decent chance that it's something textually interesting, and it's not a guarantee, but it's a decent heuristic, it gives you good results. And this is code that just goes through and implements that heuristic here. You give it an array of bytes and it goes through, it splits that array into chunks of continuous printable ASCII characters and then filters out all the short ones and gives you back the long ones. So we can run this on the byte arrays that we get out of the scanner to see what's going on in them. Alright, so those are the foundations of the program, there's a bunch of bookkeeping that goes on in it if you're interested in that part look it up on Github, but those are the fundamental pieces and we now know how to build all of that. And so we can read all of this stuff, but we wanna be able to actually output it in some form that's nice for the human to look at. So we could just dump it all in text form or something like that, but it's gonna take a lot of work to interpret. Ideally we want something more like this. And as an intermediate step I produce something like this which is not very readable at all. But this is an open source program called Graphviz and essentially you give it a list of nodes and you give it a list of connections and you say this node has this label and it's connected to these nodes and this node has this label and it's connected to these nodes. And when you hand it over to that program it hands you back stuff like this which is really cool and readable and you can go through and look. This is, I wrote a simple C program that creates a little structure in memory and then hands it off to my dumper program and that generates the Graphviz stuff and then Graphviz turns it into a PDF which looks like this. So we can go through, we can read, we can see we started off with a pointer up at the top, that pointer points to some malloc memory which contains this and those point to more malloc memory which point to more malloc memory and we've got a couple of strings at the bottom and we can go through and you can just see this whole structure visually, which is cool, so that helps us figure out what's going on. So that's the theory of how we're looking at these things. So let's actually go through and look at them and see what's going on with this stuff. How does Swift represent things in memory? How does C represent things in memory? How does C++ represent things? So quick notes, this is all very architecture specific, I did this stuff on Mac on x86-64, iOS 64 bit is likely to be very similar, Swift on Linux 64 bit is likely to be similar, but this is stuff that's very useful for debugging, it's very useful for understanding how the system works, it is not a good idea to write any code that relies on this stuff unless it's kind of a hobby project or an experimental thing. You don't wanna write any production code that relies on this stuff because offsets, sizes, the meaning of various fields is all subject to change from one release to the next. So it's really useful stuff, but you don't want to incorporate this into that library that you're writing for work that's gonna ship to users. Oh, my phone is not cooperating with me today. There we go. Alright, let's take a look at some C structs. C is very simple in how it lays things out in memory, that's kind of it's appeal. And we'll take a look at this real quick, I made a C struct which just contains three long fields, x, y and z, I wrote a little bit of code that fills them with one, two and three and then I dumped out that memory using my nice graphical dumper and that's what we get up here in the bubble. And you can see that it essentially just lays them out sequentially. We've got one followed by two followed by three and there's a bunch of empty space because long is an eight byte value and these are small numbers, so they have a lot of leading zeroes and just puts them out one by one. It gets more interesting when you get different sizes. So this is a struct that has a bunch of fields of different sizes, a through h, some of them are one byte, that's a character, some of them are two bytes, those are short, some of them are four, that's integers, and some of them are eight bytes, that's long. And again, the compiler just lays them out one by one, you can see one, two, three, four, five, six, seven, eight, but if you look closely you'll see that some of them take up more space than they maybe ought to. Number three for example, three is one byte, corresponds to c, that's a one byte field, but if you look here you've got three followed by zero followed by four, so there's extra space in there. The reason for that is that struct fields get padded. The idea is that it's more efficient to access data when it's on a memory address which is divisible by it's size, or at least which is divisible by whatever the hardware architecture likes for it to have. Typically it's its size. So a two byte value wants to be on an even numbered address, four byte value wants to be on an address divisible by four. And what the compiler does, this essentially wastes memory, but it trades off memory against time by expanding these fields a little bit, adding some space between them when necessary to make sure they all line up nicely so that they're fast to access. And that's really it for C. C kinda has structs and that's about it and it just lays things out sequentially and there's no metadata, there's no implicit pointers or anything like that. C, what you see is what you get. C++ gets more interesting though, here's a simple C++ class, I've got three virtual methods on it. It's got one field, I create one and initialize it and dump it out, and this is what I get. So we can see now it's not just one bubble, it's got a bunch of different stuff. And I'll zoom it in so we can actually see what's going on here. Up at the top is the actual object and that's the thing I created which contains, in it's single field it contains the value one. And we can see that it's got more than that. So it just had one field, but here we've got more stuff at the top. And the program explored this and found that that thing at the top is a pointer which points here, and then that points to more stuff. And so that thing at the top is a vtable pointer. So in C++ the way you do virtual method dispatch is the first pointer sized chunk of an object is a pointer to a vtable, which is a table of function pointers. So when you call through to something like object.x, what it actually does is it uses that table to look up the implementation of x for that object. And that's how inheritance is implemented. If you subclass something and override, then that generates a new table and that new table contains new entries for those method implementations so that the code knows what it needs to call. So here's an example of that. Quick C++ subclass, it inherits from the previous one, it adds a new field, it adds a couple of new methods. And when you dump that out you get a little more stuff. And again, I'll zoom in. So here we've got the object at the top, like before you've got this vtable pointer and then you've got the fields. And if you'll remember, field number one was from the super class, field number two is from the subclass, it just puts them sequentially. So the idea is that when the super class is doing stuff it can look at it and it sees what it thinks is itself and then the subclass data gets laid out afterwards so there's no conflict there, but they're just efficiently packed in memory just the same. And then the vtable for the subclass gets longer because there were five methods now, we had three from the super class, two from the subclass and then it just lists them sequentially. So every method just gets an index in this table. And the subclasses get the same table as their super class, except they can be potentially longer if there are more methods added and entries get replaced to indicate overriding. Let's take a look at multiple inheritance, this is where things get interesting. C++ allows a class to subclass multiple classes simultaneously. So here's a second super class to go along with our original. And here is a subclass of both. So each super class some methods, each super class has a field, subclass has a field, create it, fill it out with some data and this is what we get. It's a little bit more complicated. The good news is that most of that is runtime type information stuff that we can kind of not look at too hard. Let's zoom in and see what's going on. So again, object is at the top and we can see that it starts out similar. So it's got a vtable pointer followed by that first field, which is one, but then something interesting happens. Instead of doing just one, two, three, laying out all the fields sequentially, we get another vtable pointer right in the middle of the object. And so this is how C implements multiple inheritance. We've got one vtable pointer at the top, we've got another one over there. And the idea is that it's kind of like two objects glued together. So if you take this first one here, that's the vtable that indicates it's an instance of that first super class, and then the second super class gets laid out below it. And what happens normally in C and with simple C++ classes if you cast between types it's got an instance of a subclass, you say, treat this as if it were an instance of its super class. This is just like some bookkeeping trickery, right, you've got the exact same pointer and you just say, okay, pretend this means something else. But when you get multiple inheritance involved suddenly things get a little more complicated. And if you say, take this pointer and interpret it as a pointer to its super class, what it will actually do is it will move that pointer a little bit. So in this case it's going to add 16 to that address and give you a pointer into the middle of this object. And because that vtable is right there, it all just kinda works out. And it's a bit of a crazy system, but it gets the job done. And so you can see the effect here where you've got essentially the vtable for the subclass and each part of this object, you've got two vtables in the object, each one points to a different part of this vtable and everything just kinda lines up with these multiple super classes so it all just works out. Lots of compiled time trickery and then the end effect is at runtime everything is nicely laid out, friendly, and quick. Friendly for the computer, not for us, but that's usually okay. So that's C++, you get crazy stuff with multiple inheritance, but it's usually straightforward. Again, you get that vtable at the top which tells you what kind of object it is and then all the fields are just laid out. Sometimes you get padding depending on their sizes, but it's just one after the other, after the other. Just in line. So let's move on the Swift now. And Swift starts out very simple like C and like C++. So just to get the ball rolling I created an empty struct and you'll never guess what it looks like, an empty struct contains nothing at all, it's a zero size object. Interesting feature of this, it does still have an address in memory even though it doesn't contain anything. The compiler still gives it an address which I thought was kinda funny. It probably doesn't make a whole lot of sense for the compiler to optimize for zero size structs since we don't use those very much. Move on to a more realistic example, more useful example, here's a struct with three fields. This is essentially the Swift equivalent of that C example I did with the beginning. Three fields, one, two and three, and it looks, this is the result, the output, the way it's laid out in memory looks a lot like the way it was laid out in memory in C. And in fact it doesn't just look like it, it is exactly the same. These two are laid out in exactly the same way. So Swift is just laying it out one, two, three in a row like that. There's no fancy metadata going on, there's no extra stuff, it's just your fields. And then I did the same thing that I did before with the multiple sizes. And again, we get the exact same result. So this is a complicated struct with different fields of different sizes and the output is exactly the same as it was in C. With one exception, you'll notice that after three, you get one, two, three and then there's this five f thing before four, that's just because the padding that gets inserted does not have to contain any particular value because it doesn't mean anything. The padding is ignored. So before when I ran the thing on C it just happened to contain zero and when I ran it on this it just happened to contain five f. So this is kinda like the junk DNA inside your program. But again, it's just laid out exactly the same way C is, so there's no overhead, it's very straightforward. Let's look at a little more complicated thing, let's see how a Swift class looks. So simple thing, complicated result. It's not as bad as it looks. Essentially what you're getting in there is that Swift has this whole hierarchy of stuff and it knows what types mean at runtime. And I wanna zoom in a little bit so we can see the object, but what all this other stuff is going on is essentially it's saying that your class is actually a subclass of this heap in the Swift object class and then that class has a metaclass and all that stuff. So there's all this metadata that's going on that you can use to inspect objects and things like that. But we can mostly ignore it. So if we ignore all that other stuff and kind of zoom in, we look at the instance of the object here and we can see the data laid out in memory, one, two, three, and there's a header above it which is similar to the way C++ was. In C++ we had a vtable and then there were the fields, and in Swift we have an isa pointer, which is essentially the Swift equivallent of a vtable, it points to the object's class, then we've got some other stuff which I'll talk about in a moment and then you've got the fields. So you've got the same arrangement of a header followed by the fields just packed in memory. Nice, linear, fast, hardware friendly. And let's take a look at a little bit more complicated class, this is the class equivalent of that struct that I showed. And it ends up being the exact same thing with that sort of header put at the top. So you've got that isa pointer, you've got this other stuff which I'll get to. And then all the fields are just laid out the exact same way they would be in a struct. Sequentially with some padding to make everything line up nicely. So this is sort of the visual representation, the abstract representation 'cause those hexadecimal things get painful to read after awhile. So this is what they mean if you actually go in and interpret it. You've got the isa pointer, that other header field that I didn't mention yet, those are retain counts, you may or may not know Swift operates using automatic reference counting. So it needs to count the number of references to each object and in Swift those counts are stored in the object itself as that second header field. And then your stored properties just get laid out after that, the compiler just puts them one by one. And I did say retain counts, plural, so there are actually two counts in a Swift object, this is in interesting little feature of the way the system works. There's the strong count and the weak count. So when you make a normal reference to a Swift object that increments the strong count and then if you make a weak reference to an object that increments the weak count. And the idea is that when the strong count goes to zero, if the weak count is non-zero then the object is destroyed, but it's not deallocated and that could be a talk by itself. I got a blog article about it if anyone cares about exactly how that works. But that's essentially what we're seeing there. So there are two separate counts packed into the same field. Each one I think is like 31 bits or something like that. And then let's look at that isa structure. So that isa structure in C++ the vtable was just a list of method pointers. In Swift it's a little bit more complicated partly because of Objective-C interopp. Swift has to work with Apple's Objective-C stuff and in fact all Swift classes in memory are also Objective-C classes. This fact is hidden from us sometimes. If you explicitly subclass in Objective-C class then you can see it, if you use the at obj C annotation you can see it. But even if you do none of that and you do what looks like a pure Swift class, it's actually an Objective-C class just the same. And just to be a little bit more accurate, the first part of an object is not necessarily the isa pointer, sometimes it's the isa pointer along with some other junk. This is just a way to sort of efficiently pack some metadata in there. Apple does this on iOS 64 bit, I don't believe they do this on the Mac currently. This is all subject to change, but basically they can put little extra bits of information in there like whether this object has ever had any associated objects with it that need to be cleaned up when its deallocated and things like that. Just real quick detail there. So what do these class structures look like? Since every Swift class is also an Objective-C class, that means that we can look at Objective-C class definitions to see what's going on. And Objective-C class definitions are part of the Objective-C runtime which is open source, that's convenient. So we can just look in runtime.h in the open source dump there. And if we look there and we see what's going on, this is what we get. So every class is also a valid object in memory. So if you remember an object starts with an isa pointer, so that means every class starts with an isa pointer as well. So every class is also an object, a class has a class, that's called the metaclass and you can follow that rabbit hole all the way down until you get very confused. The class also stores super class. So that allows you to follow the chain up and essentially explore the class hierarchy. Class stores its name, it stores a bunch of other stuff, it stores how big it's objects are, it stores a list of instance variables and methods and it's got a cache which speeds up methods dispatch. And then Swift classes take all of that and they add more stuff because Swift has more stuff going on. So if you look in the Swift open source to see what's involved there, we've got some flags, we've got this offsets, a lot of bizarre stuff. But essentially a Swift class is the Objective-C class with more stuff on the end. And then, this is an interesting part, after all of those fields it's a list of methods again, an array of method implementation. So essentially it's the C++ vtable approach again with some extra stuff at the top that we can ignore. And so what that means is that when you do a method call in Swift it translates into essentially an array lookup. So you write obj.method up here and that translates into code like this down here. So essentially you take to object, you get that isa field out of it and then you just index into it to get the method pointer and then you jump to it. You essentially make a function call based on that. And so it's quick, it's efficient at runtime. Let's take a look at what an object looks like when you subclass a bunch of stuff. So I made a class, a subclass, a subclass of that and so forth four levels deep. And it looks exactly the same. So you've got that isa pointer at that top which tells you what it is, you've got the retain counts below that and then the field of all those classes just gets laid out sequentially. Just like in C++ we saw before. So at runtime it's very simple. Even though the class hierarchy we looked at was kind of long and complex. Let's take a look at arrays in Swift. Arrays in Swift are value types which means that they act like primitives essentially when you assign x equals y that conceptually creates a new array which is codally separate from the original, this dump reveals that's essentially a lie, they are, in fact, reference types under the hood the way they're implemented. So this array, one, two, three, four, five, if you actually look at it it's just a single pointer. And that points to one, two, three, four and then five after that which ran off the end so it got truncated. And so what's going on with that is every array that you work with is actually a pointer to the storage and when you make a new array you just get a new pointer to the storage, nothing really happens and it's only when you actually modify it, it will go in and it will see, oh, someone else references this, I will create a copy and then modify that copy. So it still references under the hood, you just don't see it until you run a program like this, then you see it. Let's take a look at protocol types, this is an interesting aspect of Swift. So here's a Swift protocol, it's got three methods in it. Here is a struct, which holds three instances of that protocol, right, you can use a protocol as a type itself and that can hold an instance of anything that implements that protocol. Here is a struct which implements it, it's just got empty implementations of those three methods. It's also got a field which is just an integer containing the strange hex value, that hex value will spell out the word small in ASCII, basically that's there so that when I do the dump we can identify it because it will search for that string, it will show it in the printing. Here's another struct, this is a larger one, it's got four fields. The first one spells out large and the other ones just contain one, two, and three repeated just so that they show up nicely. And finally, here is a class, if this wants to advance, yeah, my wifi is not cooperating. There's a class which, same as the struct essentially except it spells out class instead. And so we wanna see how these get represented. So here we create an instance of that protocol holder containing one instance of the small struct, one instance of the large struct and one instance of that class. And if we dump it out here's what we get. The larger view of this is very complicated, but we can see that struct in the list of strings that it found, it found small. So we can tell from this that that small struct actually gets stored inline, that protocol, that field of protocol type is able to store that struct inline, but the large struct does not get stored inline and of course the class doesn't because the class is a reference. And where did that large struct go? Because structs normally get stored inline, but this one was large, it ends up getting stored off on the side here if you chase the arrows around. And essentially what happens is it's too big to fit. The compiler can't know how big these things are gonna be so it places an arbitrary size limit and when you go over that limit then the compiler behind your back boxes it up, allocates something dynamically and stores it over here. So here large gets stored off in the weeds somewhere. If you chase it down you actually look at how these things are implemented. A value of protocol type holds five fields. It holds three arbitrary data fields and then it holds some type metadata which essentially tells you what it is and then it holds a witness table which is like a vtable for the protocol. And those three data fields are given over to whatever the type needs them for. So if you've got a struct which holds that much stuff or less it gets stored inline very efficiently and everything is quick and as soon as you go over that limit, suddenly it has to get broken out, it gets boxed up, it gets allocated dynamically and you loose a lot of efficiency. And this is all hidden from you, you don't notice it util your code gets slow. So the witness table is basically a vtable, it's just an array of implementations just like the C++ vtable. And so that means that when you make a method call on a protocol type it looks a lot like a method call on an object 'cause you've got this special table just for the protocol. So when you make a call you get a protocol type like that, you do p.g, make a call, it translates into something like this. You just take that, you look up the table by looking up the fourth word in the protocol and then you use the offset in the table that you know about because the compiler just knows that it's that method and then you just make the call based on the function pointer. And then if you have a struct that's too big it ends up looking like this, instead of having data fields, that first data field is actually a pointer to the real data. So everything gets stored off over here, you've got the table over there. And then the methods here know that when they need to do their stuff they have to go up and chase that pointer and it's all just handled behind your back. And this is not cooperating again, indeed, there we go. Enums are a very interesting case in Swift. So Swift has these high level enumeration types where you can have associated data and all that or they can just be very simple things. Here is a simple case, just five cases, nothing associated with them, just A, B, C, D, and E. Here's a struct which will hold those. And the result is those get laid out very succinctly, zero, one, two, three, four, each one gets a different number, they're one byte long because we don't need more than one byte to represent five values and it's all very nice and compact. Here is a version with a raw value so you can actually go through and tell Swift, I want my cases to correspond to specific values. So what this does is it says A is one, B is two, C is three, D is four, E is five. And let's see what that looks like. And an interesting thing, it does not change. It doesn't go one, two, three, four, five, it still goes zero, one, two, three, four. Alright, running out of time here. Real quick, if we just go through (clicks) for a string, you can do string raw values. So A is whatever and then B, C, D, and E get defaults, those are just B, C, D, and E. And those still are zero, one, two, three, four. And essentially what's going on here is the raw value can be stored off in a separate table somewhere, the compiler knows about it, there's no per instance raw value of any kind, so it can just be zero, one, two, three, four and somewhere else there's a table that says zero is whatever, one is B, two is C, and so forth. Alright, let's look at associated objects real quick. This is just an enum, the first case has an object associated with it and the others do not. And if we dump that out we find that it has expanded because it needs to be able to store that object pointer, but it has expanded intelligently. So the first thing is just a raw object pointer and then the other ones are just small integers. And the compiler's able to pack these so that it knows zero, two, four, and six can never be a valid pointer. So it's able to use that to distinguish between those. And then if we make it larger we have an enum with A, B, C, D, and E where they all have objects associated with them and suddenly everything gets bigger. Every entry is a pointer followed by an integer. So object pointer zero, object pointer one, object pointer two, object pointer three. So the number that gets assigned to each enum case and the associated value essentially get laid out next to each other. The compiler's able to pack them compactly for that one specific case, but not in the general sense. Alright, so wrapping up, I'm just gonna kinda skip through these real quick since we're behind on time. We've got real physical memory, we've got conceptual memory and then we've got sort of the actual, the architecture of it all. C just lays things out nice and straightforward with a little padding. C++ objects get a vtable at the top. Swift objects get the same sort of thing, but with more stuff going on. Protocol values end up taking up five words of memory, sometimes they can store data inline, but if you get too big they don't. And enums end up being packed in many different ways depending on what's going on. There's our quick sum up which I just said. And so you can learn a lot by poking around. It's a lot of fun and sometimes it's useful. And as they asked me to remind you, and as I did before, remember to rate the session in the app and that's it. So if there's questions we can, or if we have no time for questions or (laughs), okay, can you use Swift demangle with PO in Xcode when debugging? I don't think there's any built in way to do that, but what you can do is just copy, paste onto the command line, Swift demangle should be available in the terminal. If you wanted to I'm sure you could build a little script, LLDB is scriptable through Python so you could do that if that turned out to be useful. Yes. Okay, so the question is, any versus any object in Swift three and whether there are changes based on whether you import foundation? I don't think there would be changes in the layout based on what you import because there needs to be cross-compatibility between files that import foundation to files that don't. So they would still need to be the same. But any, in Swift 3 Objective-C objects as untyped objects now come in as any instead of any object, so there's definitely a change there, I believe that's just essentially a translation phase. Any I think looks like one of the protocol types where it's a five word thing, it's got three inline and whatever and it's essentially storing things that way. And then there's just a step where it takes that Objective-C pointer that comes in and just kind of puts it in one of those things. The source code is online for my thing, it should run more or less out of the box, so if you wanna experiment and see what it does feel free. Anything else? No? Alright, oh, yes? (inaudible) Sure. Yeah, so question is about the new memory debugging facilities in Xcode 8 and how it compares to my stuff. So that new memory debugging stuff is really cool, you can go through and it will just essentially show you kind of graphs like I showed here except they're live which is really neat. And I haven't played with it a ton, but I'm sure it's gonna be really useful. It's a little bit more limited from what little I have done with it, in that it tries to, well it's gotta work at runtime, so it has to be kind of limited in that respect. I believe it will not trace like, C pointers and things like that, at least not beyond a certain point. It's not gonna be tracing global symbols and things like that. But as far as like, looking at plain Swift objects, it's really cool, it'll show you the trees, it'll show you this object points to that object. And I think it's gonna skip over things like the pointers up to the classes, so it doesn't give you everything. But for what you care about day to day, it looks really cool, really useful. Alright, looks like that's it. Thank you very much for coming and enjoy the rest of the conference. (applause)
B1 中級 GOTO 2016 - 探索Swift內存佈局--Mike Ash (GOTO 2016 • Exploring Swift Memory Layout • Mike Ash) 82 6 colin 發佈於 2021 年 01 月 14 日 更多分享 分享 收藏 回報 影片單字