CppCon 2017: John Lakos “Local ('Arena') Memory Allocators (part 1 of 2)”

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
- And I guess we're gonna get started. Hello. My name is John Lakos. I have been at Bloomberg for now about my 16th year. Prior to that, I worked at Bear Stearns, which is no more, and that is where this notion of allocators became a real thing. And prior to that, I worked at Mentor Graphics, where I used memory allocation for a long time and actually talked about it in my 1996 book. So we've been dealing with allocators, we meaning me, for a very long time. And this is not new to me. But some of you may feel that this is new. It's not new. Also, people in the video games area know this stuff better than I do, and they use it all the time. And it is not magic, and it is not syntax or any of that stuff. It is absolutely essential in order for them to do their jobs. And the way I came on it, it was absolutely essential in order for me to do my job. So if anybody doubts that this is real, by the end of this talk, there should be no question at all that it's real. The only thing that you should be left with is to make the decision, is it worth the effort? The good news is, it actually is worth the effort, and when we get to C++20 and 23, the effort will go to zero. So that's good news. And then you'll start to worry about, "Well, what about the performance cost?" Ha. Don't even get me started. Now, there's a lot of material in here. Normally when I give a talk, I have like five, six, seven, eight hundred, nine hundred, maybe even almost 1,000 slides. You'll be pleased to know I don't have more than 500 slides. The bad news is they're animated, which makes them like 5,000 slides. (audience laughs) We have two hours, though. Hopefully I'll finish early. I don't mind if you stop me to understand the material. There's gonna be enough time at the end of the second talk for you to grill me on any concerns that you have. I look forward to it, I really do. So any questions that you have about what I'm talking about, please do interrupt, and I will try to remember, Mr. Sign Man, to repeat the question because I need to do that. Does anybody have any pre questions or anything about the rules, which is, if you don't understand something, you stop me, but if you wanna heckle me, wait 'till the end? Okay, good. All right. The first slide is there because Bloomberg has a standard slide, but if they thought, or anybody thought or if it were even possible to go in and change all my animated slides to these Bloomberg slides, that was not happening. So somebody just put it back. This is the Bloomberg slide. Everybody. (inhales deeply) Okay, that's done. And this is John's slide. We're gonna use John's slides from now on. All right, this is the Copyright. It's there because it has to be there. This is what we're talking about. Are they important? Are memory allocators really worth the trouble? What situations can we use them in? How can we apply them? What's the performance impact? By the way, performance is extremely important when we talk about allocators, but surprise, it's only one of three areas where allocators are very, very important. So we're gonna talk about performance today, but if I were gonna argue the value of allocators, I would have two other complete talks about why they're important. But today is just performance. By the way, at the end of this, you won't care about the other two. So here's what we're gonna do. I'm gonna talk a little bit about background, because this is, if anybody saw Bjarne Stroustrup's talk where you need to know about the layers, you can't deal with the highest level abstraction, you have to go down to the libraries and perhaps even to the operating system and perhaps even to the hardware, guess what. This talk is gonna have a little hardware in it, unfortunately, so we have to do that. But we shouldn't be embarrassed about that, right? We should be proud of that. We're gonna look at the hardware a little bit. Even the hardware that I'm gonna show you is an abstraction, because the idea, it's the gist of it. It doesn't matter. If we get too precise, then we have to look at different processors. We don't want to look at different processors. We want the gist, because it is a very rich, robust benefit that we're gonna get, so we're gonna keep it above individual architectures. We're gonna talk about a general architecture, although we will run benchmarks on a specific architecture. We have run them and they're published on GitHub for many different platforms, different architectures, different compilers. The results are more or less the same. You couldn't possibly care. Your eyes would glaze over if I showed you all of the data we have, so I'm not going to. Let me show you some stuff. So, why do we like C++? Well, a couple of things. It enables us to fine tune at a low level, and it can deliver a very high runtime performance when needed. That's why we use it, right? Otherwise, we'd use something else. Why should we care about memory allocators? Well, they enable us to fine tune at a low level when needed, and they can help improve runtime performance. So it seems like allocators might be a good match for C++, right? Seems like it, right? We want high performance. But no, we can't be bothered with the platform, so just forget about allocators. Doesn't work that way. It really doesn't work that way 'cause there are people who will not use the standard library without allocators because they can't. I was in such a position a long time ago, and I didn't use the standard library. What are the benefits? Not all memory is alike. Different kinds of memory. That's one of the benefits. You can deal with different kinds of memory. You don't have to say all memory is alike. Another one is we can do testing. We can do debugging. We can do measuring. We can do profiling that we couldn't otherwise do. If you talk to somebody who runs a large system, the profiling is the most important thing, not the performance, but the profiling, because when something goes bad and you need to figure out what's wrong, this is how you do it. When memory is getting sucked in somewhere, what do you do? Really, honestly, this is how you do it. Oh, yes, runtime performance. That's very important to some people. Not everybody, maybe not even half the people. And given that allocators today require some effort, and we'll see that it's not as bad as people think, but that's not the purpose of the talk. Purpose of the talk is simply to explain that they actually are worthwhile. The next step is to figure out how to deliver them at the least possible pain point. C++11 is the most possible pain point. C++98 is they don't work. Bear Stearns, circa 1997. I invented, I, taking credit, invented what is now the C++17 memory model. Make no mistake, it doesn't look like what I did, because what I did was so obvious and simple that anybody could understand it, and what we have in C++17 is a vast improvement over C++11, which was a necessary painful step because people had fear, uncertainty, and doubt about polymorphic memory resources. No, it had to be part of the type, 'cause that's all we know. You know, fear of the unknown. By the way, fear, uncertainty, and doubt, that's FUD. There is a lot of FUD going around. We need to debunk the FUD. That's what the fun questions are gonna be at the end of the talk. You're gonna say, "But it isn't it (mumbles) like this?" Nope. All right, so we're gonna see that, but it'll become clear. So back then, we had coalescing allocators, and so somebody came to me and said, "I have a model, and I build up this model. "And I use it. "it's great. "Then, when I go to destroy it, "it takes 10 seconds to reclaim the memory." Can you imagine 10 seconds to reclaim the memory for a model? So it turns out that because we had coalescing allocators, the benchmarks were all based on how fast we could allocate the memory, but they didn't care because you'd just leak the memory and it goes away, and who cares? So to actually reclaim the memory was the big bottleneck. I gave them a local allocator using my model from 1997, and instead of taking 10 seconds, you couldn't see it on Purify because it didn't exist. That's the kind of improvements we're talking about, from 10 seconds to zero. Another one, Bloomberg. Whoops. Did I go a little fast here? Hold on. 2002, Bloomberg. We have a way of doing swapping to disk. The way we do it is we have something called finch memory. And so we have memory mapped IO. We store things that we wanna save in a process to disk. And because all of our processes are the same, we can read them into any other like process on the machine, and we're reading in binary, but it can just come back the way it was. You can't do that without allocators, and you can't use the standard library unless you can provide a finch memory allocator to take and put your memory, right? So we need to do that. That's the placement aspect. And then, in 2006, people realized that you can use allocators on the front end to make the user interface zippier. That's something people observed. And people who were not at all happy with the way I did things at Bloomberg said, "That's the best thing you've ever done." I'm like, "Okay, thank you. "I'll take it." So what are the common arguments against? I'm sure I don't need to give you arguments, 'cause I'm sure you all have arguments. "Oh, allocators, not worth it." Well, they require more up front design. Well, software engineering is about up front design. There should be a payoff for it, of course. Complicates the user interface. Really? How much? How are you designing it? These are all fair concerns. May degrade performance. If I try to use an allocator here but I don't really need it, it's gonna slow me down. I'm sure you've heard that. Suppose we don't need an allocator. We don't need a special egg. Why should I worry about it? And that's most of the time, right? 'Cause when you're using C++, performance isn't typically important. It's only sometimes. So why should I worry about it? Maybe I choose the wrong allocator and blow the whole thing up. Do we have to actually know how to program, or can anybody write C++? I don't know. Anyway, so these are all valid concerns, and we can deal with them only by well supported facts and careful measurement. Does that sound reasonable, as opposed to fear, uncertainty, and doubt? That ends here. There is no doubt, none. You're going to be shocked at how little doubt there is. We're gonna talk about some background material, like main memory. What does main memory look like? Well, we have the CPU and we have main memory. Everybody knows this. This is three levels down the stack where we have hardware. I want you to appreciate the amount of time I put into drawing that. You see the two gradients there? That's the wear from that wheel turning. Now, I'm gonna start it up. Hold on. First, we're gonna do some memory. We're gonna put some memory there. Did you see it turn? (audience laughs) And then we're gonna put some more memory. Oh, it turned again. I hope you're enjoying this, because I got very frustrated with how much time it took to do this. (audience laughs) So I kept it up for just long enough for you to know that I can do it, and that's it. (audience laughs) (audience applauds) Thank you. Applause are welcome. Then, I'm going to do something that is unthinkable. I'm gonna add a cache. That will never happen again, that thing over there. We're done with that. (audience laughs) That's staying fixed. And so now we're gonna add something, and you'll notice that what happens is we get a cache line. And everybody has heard the term cache line. For those few of you that have never heard of a cache line, it's a chunk of memory, contiguous memory, that gets pulled into the cache all at once. And so the next time you ask for something on the cache line, you already have that. So watch what happens. That's one cache line. Here's something from another place. That's another cache line. The cache can take something from anywhere in memory and put it there, so now you have these two things. But now, when we add something else up there, it's instantly in the cache, 'cause it was already there. And so on. So we just keep doing this. And eventually, we might write to the cache like that. And then we have to save it. Now, there are all kinds of algorithms and everything. Please, let's not worry about it. The idea is that has to be put back to main memory, and then we'll just get rid of it from the cache. And now that space is available for something else. Look there. And so we get a cache line, and it goes there, and that's how a cache works. And there are many levels of cache, and we don't care. Then there are things at a higher level, like paging. So when you have so many pages in your real memory, then you have to page out. And that's kinda like a cache, because it goes to a slower memory. So we have all these different levels, from registers to L1 to L2 to L3 to virtual memory. All of that, right? What you need to know is, things that are accessed frequently in some block of time need to be close together in memory. If you can deal with that concept, that's locality. That's good. Not having locality is bad. Can we all just accept that? Because we should have known that for the last 50 years. Okay, good. Ah, so now we're here. We have an executable... Did I jump over something? Oh yeah, main memory segments. This thing is going a little funny. All right, anyway, this is what happens. We load in a program, and it becomes an executable program, and then we have stack memory, which grows down typically, let's say, and dynamic memory, which grows up. And it can be in the other direction. It doesn't matter. But these are the two ways we get memory other than static memory, and static memory, of course, lives in the executable program itself. There are all kinds of different static memories that are more efficient and less efficient, but it's not important. So what's a memory allocator? So, a memory allocator is something that, well, allows us to control memory. With C language memory allocation utilities, for example, we have malloc and free. And malloc and free work on dynamic memory. Now, we have another one some very smart people who really cared about performance back in the day decided that this thing called alloca was important. What that does is it gives you memory on the hot program stack. And that's really good if you know what you're doing. If you don't know what you're doing, your program will crash and burn. That works on bits of memory in the stack. How am I going with speed? Everybody with me so far? Okay. I don't wanna lose you. So, this is a first cut at a memory allocator. A memory allocator organizes a region of computer memory, dispensing and reclaiming authorized access to suitable sub-regions on demand. Might not be contiguous regions. That's okay. But your first thought is, it's a block of memory, and I'm gonna get stuff out of it. All right. So we have general purpose allocators and special purpose allocators. And you can imagine a general purpose allocator works for everybody, and it does a good job all the time. And a special purpose allocator doesn't work for everybody, but it works for some people real, real well, and so you say, "I know more than the general purpose allocator. "I'm gonna do this." And if you get it wrong, the whole thing blows up. So if you don't know what you're doing, don't use a special purpose allocator. But if you do, you can beat the compiler, hands down. Then we have a global allocator and a local allocator. A global allocator is good for the duration of the process, and a local allocator isn't necessarily good for the duration of the process. The global allocator is accessible from everywhere, and it doesn't need any reference or memory because it's global. It would just go there. Local allocator, you have to provide a handle so that it knows what's going on. And the local allocator doesn't talk about all of memory, but a subset of memory. And it doesn't talk about the entire duration of the process, but only a subset of the duration of the process, so it's a little time space box. You can use this memory right now, just this memory and just for now. That's a local allocator. So in C, we have malloc, and in C++, we have new and delete. But those operators are not allocators. They are ways we access allocators, right? They're not allocators. They're syntax. So now we have global and local allocators and general and special purpose allocators. Look, we have some things in the box. And you notice that malloc and gree and new and delete are global, general purpose allocators. But they're not really. They're just specs for general purpose allocators. Now, tcmalloc and jemalloc are not specs. They're actually allocators. They're general purpose allocators. And some might say, "Well, if we have those, "we don't need local allocators." Some are wrong. Then, we have special purpose allocators, and they could be global. They could be globally accessible, but that would be pretty dangerous, but not necessarily. If you're in a program that's single threaded, do you really need an allocator that does synchronization, or can you just say, "I don't need that, "because I don't have multiple threads. "So why do I need synchronization?" So that might be a reasonable improvement, and it turns out it is. You don't need synchronization. Why on Earth would you pay for it? Okay. Local allocators. Again, if you know that you're about to do something very intensive, "I'm gonna go over here "and I'm gonna work on this for a while "and then I'm gonna stop," local allocator is your friend, because all the memory, all the stuff that you're doing is right there. That means the number of cache lines that are in play are much smaller, which means the number of hits you're gonna get in cache is much greater. And it applies the same to pages and memory and every level of cache up and down the hierarchy. It doesn't really matter why you're getting it. The key is locality is king. So if you're gonna access something in rapid succession and you can do all of it in a boxed region, you win. Otherwise, you lose. Does anybody have any questions on this? Going too fast? Locality is good. That's a fact. Okay, good. A memory allocator is a stateful utility or mechanism that organizes a region of computer memory, dispensing and reclaiming authorized access to suitable sub-regions on demand. So here's a local allocator. And the computer knows how to access that memory. The allocator itself has some logic in it, but it's tied to memory. Now, some people seem to think you can copy an allocator, and if you think you can copy an allocator, I got news for you. No. An allocator is, the important part is that it's a region of memory. You can't copy it because it's physical. It's not a value. It's memory. Right, okay? So we know about value types. They represent values, and I have talks on that out the whatever. But this is hardware, and all we're talking about is a pointer. Every allocator that you ever deal with is a pointer to some region of memory and some logic that controls it. The logic lives in the object, but the memory lives in memory, and that's a region. All right. So here's a local allocator, and you notice it has a default constructor, like any value type, but it's not a value type, so don't get confused. And what it probably has, it's not a default constructor, actually. It takes the region of memory that it's gonna operate on, and then the rest of it is the logic to allocate and deallocate. And then, this poor clicker was doing things too quickly. To allocate and deallocate, and those are the operations that we normally have. Now, I know there's alignment and all of these wonderful, good things that we could talk about. We're not gonna talk about them today because they don't fit on slides. So we're just keep it real simple, because we can understand that, and then we can extrapolate to the reality which is just a little bit more complex. But what's nice about local allocators that's not true of global allocators is we have this third thing. This third thing is called release. So while new and delete don't have any notion of release, nor should they, a local allocator, you can say to the local allocator, "You know on that memory you were managing? "Forget about it. "It's over. "Just give it back to wherever it came from. "You don't even care what the objects think. "It's not their call. "I know. "Give it back." When you're in that realm, now you're playing with the big people, and you have to know what you're doing. But if you can save significant time in a video game by doing it, then why not? Whoops. Okay. Yeah. - [Burke] What is the purpose of this release? Shouldn't you put that in the structure? - Okay, so the question is, I will get to this more, what's the purpose of release? So we have an object. Call it your, what's your name? - [Burke] Burke. - Burke. We have Burke. Burke allocates memory. We have an allocator. Burke takes an allocator construction. When Burke needs memory, Burke says, "Allocate memory." Now you're full of memory. Now, here's John. "Burke, you're done." (audience laughs) (imitates gunshot) You're gone. I didn't call your destructor. You're just gone. Now, you're probably saying, "Is that okay?" We'll get to that. (audience laughs) It doesn't feel good to just be dismissed like that, does it? It's horrible, but it's very quick. You're gone, all of you, in one shot, boom. (audience laughs) Sorry about that. Good question. So a memory allocator is the client facing interface for stateful utility or mechanism that organizes a region of computer memory, dispensing and reclaiming authorized access to suitable sub-regions on demand. I'm trying to build up an idea. We can supply allocators in multiple ways. We could supply them as stateful utility functions, like malloc and free. But that doesn't really support allocator objects. We can't really think of an allocator as an object. It's a pair of functions. It's not, God forbid, object oriented. Then, we could think of it as a reference wrapper in a template context where we're gonna pass it into, as a template parameter, and now the type knows what allocator it uses. This is the C++98 model. This is also the C++11 model. What's good about that? Well, the concrete allocator derived type is available to the client's compiler. That might be good. Unfortunately, it forces the client to be a template in order to use it. That could be bad. It affects the type of the object, which means it's not a convenient vocabulary type, because a thing and a thing with an allocator aren't the same C++ type. I remember where I was in my house. It's a very small room on the first floor, and I was thinking, and I said, "That's a problem with templates. "They need an allocator in order, "and then the type changes. "And then what are you gonna do? "And how do you take a function? "Oh, wait, the function could be a template. "But wait, then everything has to be a template. "Wait a minute." 2004, that was not cool. It's still not cool. Or we could pass the address of an abstract base class. What's good about that? We can actually refer to a polymorphic allocator with a handle, and it doesn't effect the type of the object. Yay. The bad news is, allocator must be accessed via its virtual function interface. And people say, "Oh, can't afford that." How much does that cost? Does it cost anything? Has anybody ever measured it? - [Man In Audience] Yes. - Yes, and? - [Man In Audience] 20 cycles. - It takes 20 seconds? - [Man In Audience] 20 cycles. - 20 cycles, on your machine with your compiler? - [Man In Audience] It's average. - Averaged over? - [Man In Audience] (mumbles) Strictly speaking, it's like from five to-- - Okay, so let me just cut to the chase. So you have platforms. You did this measurement last week? (audience laughs) I wanna point something out. We did our measurements about a year and a half ago. Some platforms there is a performance cost, and some there aren't. And the ones where there are haven't caught up to the ones where there aren't yet, and as soon as C++17 adopted this, there's now pressure for people to catch up. I'll explain why that can be done, but don't assume that there's a performance cost. The other one is the object now, unlike the C++11 model, whether it needs it or not is gonna somehow have to tuck away an address. And if we assume that most things don't need their own custom allocator, now we're paying a machine address size, let's say 64 bits, in every object that allocates memory, not necessarily in the footprint but in every object that actually allocates, not one that could allocate, but in every object that actually allocates memory, we've gotta find space for a machine word size. Some people will say, "Well, that's out of the question. "We can't use allocators, "'cause that's gonna slow the whole thing down. "It's gonna make it unworkable." Have you measured it? So that's the introduction. Does anybody have any questions on this? At least we know some things we're concerned about. Maybe this is a bad idea. If it were a bad idea, would I really be standing up here talking to you? Anything else? All right. So now we're gonna understand the problem. So here's the problem. I wanna know if I should be using an allocator or not. So, should I supply an allocator? Whoops. Darn, this clicker. If the answer is yes, I'm done. I mean no, I'm done. I don't need to supply an allocator. But if I do need to supply an allocator, then should I supply it via a base class or should I bake it into the type of the class C++11 style as opposed to C++17 style? So, whether I do or not is a syntax issue. The next question is, which allocator should I use, A, B, or C? Whichever I decide, I have to ask yet another question. Do I want to bother destroying the individual objects and sub-objects that are fetched from the allocator, or do I just want to make them go away in one shot in zero time? I have to decide that. So that's where I am. This is our decision tree. And I'm gonna see if I can learn how to do this better. That's our decision tree. So we're gonna go through this process. The thing is, what's gonna guide us through this process? So, we have a tool chest of strategies, and the first strategy is we're gonna use the global allocator. The next strategy, or that strategy has two ways. We can use the global allocator as a type parameter or we can create a wrapper pointer to what's called the new delete allocator, right? So I can have an object that all it does is invoke new and delete, right? So those are the two possibilities there. Then, I have three possibilities. I can use what's called a monotonic allocator, this is a local allocator, a multipool allocator, or a combination of a multipool and a monotonic allocator, so now I've chain them together. So what the third one is is it says from new and delete, or whatever is supplied to it, I'm gonna create a big block of memory. I've got that. To that, I am going to affix a pooling allocator, and it's going to pool from that chunk. Whereas just having the multipool allocator in theory, every time you needed a new chunk, you would have to go and get it from whatever, but when you gave it back, it would keep it in its free list. That's not how we do it because we know better, so we're not gonna do that kind of thing. We actually build into our multipool some buffering that makes it ridiculously efficient to the point where nothing can compete. But that's to one side. So I'm trying to give you the slip in the truth while I'm telling you the story so you can understand what's going on. The combination of the two has applications, and either one individually has applications. We just need to know when to use what. And then, again, we can pass it as a type parameter, in which case we're baking the allocator type into the container type or whatever type needs to allocate memory. Or we can use an abstract base class and pass it in on construction, and now it doesn't affect the type at all. And then the third one, because it's a local allocator, we can either destroy it normally or we can wink it out. Winking it out means it had nothing to say about itself. It's just gone. So, that gives us 14 allocation strategies. And that is ample for this talk, and it's ample for anything you probably ever wanna do when it comes to performance. But it's not ample for anything you'd wanna do, because you might wanna do something else, like place memory somewhere or you might wanna instrument things, whatever. But as far as just performance, you're really in good shape here. You're gonna have to look really hard to find a reason to do more than this. How are we doing? Okay. So this nice little chart shows you what we got. We got 14 allocation strategies, and then we have how we're going to affix it to the container object, let's say. It doesn't have to be a container, but we'll say container, like a vector or a map or whatever. But it could be anything that allocates memory. And then we can decide whether we're going to destroy it normally or wink it out. So here are all the possibilities, nice little chart, and so let's try to understand it. So here's the normal way of doing business. So we have an allocation strategy, either AS1 or AS2, and the only difference is whether we bake it in or we use an abstract base class pointer. Take a look at this. Here's an allocator. There are no data members. And this is the standard allocator. You see allocate and deallocate. Now, notice that I put them right in line right there. That's what they do. This is not a very complex class. This is the standard allocator. It's also the global default. And we're done. That's all we're talking about. Then the next one is, let's see. I wanna use it, I just say that. And by the way, if I were to supply the allocator, the underlying machine code would be the same. So if I don't supply an allocator, we get the default allocator. If I supply the default allocator, we get the default allocator. So these have the same code. We're not gonna make a distinction. That's a type parameter. Remember, the type parameter changes the type, so STD vector with allocator is a different beast than just plain old STD vector. With a non-default allocator, I'm sorry. That's the same. Now, the next one is, okay, so, here we have our benchmark. We're gonna create a vector, and we're going to populate it like this. We're gonna do our benchmark on the vector. And then we're gonna destroy it like this. That's the old way. Now, just to prove that I know how to do this, I'm gonna use auto. And I can go back and use auto again. The purpose of this talk, of course, is not to talk about C++11, 14, or 17, but really to talk about memory allocation, so I'm gonna try to keep it very simple for those people that might not be up to speed on all of the things. It's not important. Next, we have AS2, which is the same thing, but now I'm going to have it be an abstract base class, so this is the protocol. Here's my allocate and deallocate, pure virtual functions. And you can see, this is a concrete derived type. What does it do? Now, I want you to look at this. Notice that I have a bug here in the slide. That's on purpose. The bug is what? What's the bug here? That's the bug. I put the inline there because I'm really serious that this is a derived class that has inline virtual functions. The bug is I don't need inline because it's gonna be inline anyway because you can see that the code is inline. I am not kidding. I have an abstract base class. I have a derived class. The derived class has virtual inline functions. Does anybody have a problem with that? - [Man In Audience] (mumbles) compilers-- - What? - [Man In Audience] Some compilers, if you have pedantic warnings enabled-- - With the inline? - [Man In Audience] I think so. - The inline won't compile. Repeat the question. Thank you. So the question is, if I have the inline here, will it create a warning? No, it'll create an error. It's not syntactically valid. I put it there to remind you that I'm not joking. It is inline. That's important. Everybody understands, these are deliberately inline, not just slideware. They're inline for real. There's a reason for that, because we want the compiler to be able to take advantage of it, and I am not talking about full program optimization. I'm talking about what the compiler can see today in your typical compiler scenario. - [Man in Audience] So it doesn't compile? - This doesn't compile because the word inline is there. So when it doesn't compile, you delete the word inline and then it will compile. (audience laughs) Okay, see this word inline? You see how right here there's an inline definition? Pick one. - [Man in Audience] Okay. - Okay, all right. Okay. So now, this is what we do in this world. We create an allocator, and then we pass in the address of the allocator to the PMR vector, 'cause that's how it works in the polymorphic version. That's the C++17 version. So instead of baking it into the type, you pass in the address as an optional argument, and life is good. All right. So I'm claiming the virtual functions can sometimes be bound to compile type, and the really cool thing is that in every case where that's important, they can. And if a compiler doesn't do it a year and a half ago, it probably does now, and if it doesn't do it now, as soon as C++17 is out and about and the game people get ahold of them, they will. For example, GCC does but Clang doesn't, didn't, as of a year and a half ago. So, Clang, if you're not doing it already, you have some work to do. All right. So we have normal destruction, because what else can we have? It's not a local allocator. So what does normal destruction look like? We create the thing. We go through and we build it up. You see how we're doing that. We do our benchmark. And we destroy it, and this is normal destruction, 'cause that's all we can do with a global allocator. We can't do that winking stuff with a global allocator because it's got all the memory that if we winked it we'd have nothing. Might as well end the program. Next ... I'm going slowly. Is this too slow? Do I need to pick it up a little bit? I just wanna know. A little bit faster? Okay, I see somebody say a little bit faster. Anybody think that's gonna cause, are we okay if I go a little faster? - [Man in Audience] Yeah. - All right. I just don't wanna lose you. It's a little bit of stuff here. All right, monotonic allocators, what are they? Well, they're this beast. A monotonic allocator is intended to be used, typically, with a buffer on the program stack. Here we have an example. I'm calling it bdlma::BufferedSequentialAllocator This is what we have. This was pre standardization of 17. We've had this forever. This is a really useful thing. We create a buffer on the stack of 1,024. There it is. And then, this is the buffer. Now, people know about natural alignment maybe. So if I wanna put a char in here, what happens? It goes there. Now I wanna put another char. Where does it go? It goes there. Then, another char. Where does it go? Goes there. Now I wanna put a short, and it goes there. Now I wanna put an int, and it goes there. Do you see? It has to be aligned on its address that's divisible by its size. - [Man in Audience] Sorry. Doesn't the standard require alignment-- - [John] I can't hear you. Louder. - [Man in Audience] Doesn't the standard require alignment on the largest possible, size which is (mumbles) - Okay, you asked a question, does the standard require alignment? Maybe you're talking about allocators, but I'm talking about a buffer that's raw memory, and I can put it wherever I want it. So what's gonna happen in this particular thing, it's gonna do natural alignment 'cause I said so. And it works. That's what this is gonna do. You could do it the way you just described, which is to waste a lot of space, but I'm not gonna do that, 'cause it's just me. So anyway, so then you can put the short here. Then you can put the char here. But if I get another int, it has to go here. I'm giving you an idea of what the most efficient thing is. You can back it off to your satisfaction, whatever you wanna do. That's the most efficient. We do things that work. Now, note that if you try to deallocate something, it's a no op. Here we have, we can do it as a type parameter. We know what that means. We can do it as normal destruction, so what does that look like? Normal destruction, I'm saying we have this wrapper thing, it's this thing that holds a pointer to the thing. It's just a way to build it into the type. It's just really, it's an object that's a pointer. And then we have this buffered sequential allocator. It's a monotonic allocator. We can supply stuff on the stack, we can do whatever we want, but right now, that's just gonna do, it's gonna not allocate memory at all. It's not gonna point to a buffer. It's gonna go to that dynamic thing because of the way I did it. But we could also say, "Here, please use this to start." So we have some flexibility there, and we're gonna go through, and we're gonna allocate, and we're gonna do the new separately. I'm doing them separately 'cause I want you to see the two operations. First thing we do is we allocate the memory. Second thing is we construct into the memory we just allocated. Then we come down here to do normal destruction, and you notice the first thing we do is destroy it. We destroy it, and then we deallocate it. So allocate, construct, deallocate, destroy. Right? That's the normal pattern. That's the normal way. Then there's the magically winked way. So this time, when A goes out of scope, we just destroy everything, and then when A goes out scope, the memory is reclaimed, so we don't have to do that. But the crazy thing is that we don't need to do any of that. It's legal C++ to just let those things go away. And you say, "No, it's not." And I'm gonna say, "Yes, it is." And then you're gonna go look it up and you're gonna go, "Well, okay, it's legal, but it really," yeah. (audience laughs) So normal destruction and magically winked are the same, and now we have multipool. Now, multipool is a completely different beast. And it's backed by the global allocator. So what is a multipool? It's got a bunch of different dynamic pools. It's like, I call it a beanstalk, but they're all these different, I'm gonna call them dynamic pools, but they're designed for different sizes. So now ... What happened there? That's not right. Let me try this again. This clicker is so sad. I'm sorry. So this is what happens. So during the course of, you saw with the end picture, but this is what's gonna happen. Over the course of running this thing, we're gonna keep allocating, and you'll notice the size, the chunk size that we get each time is gonna double. And you see how we're using up the chunk size 'cause they're two iterators, and it's just going until it hits the end. And then when it hits the end, we just create another thing that's twice as big and so on like that. Now, here we have it saturating, but in real life it wouldn't saturate because it turns out with the two iterators there's no reason not to keep doubling forever. Well, when you just populated a free list and went in and loaded the free list, there needed to be a saturation point. And I wanna thank Pablo Halpern for pointing out to me at C++Now about four years ago that what I was doing here is brain dead, and thank you very much, Pablo. So it's now even faster. It's not so much faster that you could really notice it, but it's aesthetically faster, and it's better. (audience laughs) So anyway, if you exceed the maximum size, then all of this stuff is gonna come straight from the allocator, and that's very important, because if it comes straight from the backing allocator, there's no buffering, there's no pooling. It's as if there's no allocator at all. So if you have a pooling allocator and you allocate a megabyte, you don't have a pooling allocator. You're just calling straight through. And that's gonna turn out to be important. All right. So, we've talked about type parameter and abstract base, and they're really the same. And then we have the combination, and the combination is simply a multipool backed by a sequential or a monotonic backed by the global, and it has, of course, you can do it by type parameter. Wait. Come on. Wait a second. Sorry. My apologies. I got a new clicker 'cause I lost my old one, and I don't know to use this one properly. Let me just come back here. So we have this one. So now, we have type parameter, and then we have magically winked. Did I get that right? Yeah. All right. So anyway, it's the same kind of thing, rather than go through the whole thing. It's just the same kind of pattern. You've gotten all the information. It's just I could repeat the same steps, so I'm not gonna do that. What did I miss here? So we're gonna have basic parameters to use to characterize our experiments. And the two we're gonna use are N for the number of instructions. So a program is gonna be assigned a size by the number of instructions it executes. And again, this is very vague. And it's also gonna be based on the number of threads, worker threads. Now, most of the experiments we're gonna do don't involve multiple threads, so that's just there sort of at the end. So the next question is, what aspects of software affect optimal allocation strategy? And this is kind of like Family Feud. So I've got these five things, and the first letter is on the front, and it'll turn out that these letters are gonna form a mnemonic. So can anybody think what those letters might mean? I'm not gonna take a lot of time, 'cause you'll never guess. Can you think of some? Just try it. What might matter? Can anybody think of a dimension that might matter? 'Cause this is something we had to think about. We wanted to think about, what dimensions of a problem would we come up with so that we could say it's got a lot of that or a little of that, and then based on that, we could make some sort of recommendation? - [Man in Audience] Fragmentation. - Fragmentation, excellent. - [Man in Audience] It's not up there. - It's not up there, but we'll get to that. - [Man in Audience] Duration. - What? - [Man in Audience] Duration, how much time you're holding memory. - Okay, duration. It's gonna turn out that that's U. (audience laughs) All right. So here we have density of allocation operations. You can imagine a program that does a lot of allocating and deallocating, right? You could imagine a program that doesn't do that at all. That's a dimension. Another one is variation in allocated sizes. You can imagine a program that allocates one size and that's it, or it allocates, every time it allocates a different size. Right? You could imagine locality of accessing memory as being a dimension. Is that important? Is the usage pattern of your program such that you could exploit locality or no? How about utilization of allocated memory? That means, do I at one shot get all the memory I need, use it, and throw it away, or do I get a little, give it back, get a little, give it back, get a little, give it back? Right? That's more along the lines of the way in which you use the memory. Is it all at once, or is it piecemeal? And then finally, just contention of concurrent allocations. So these are five dimensions. You might say, "Those are really stupid." But if you don't know anything and you have to come up with five dimensions, at least I got five dimensions. That took weeks. There you go. These are intended to be rough indications, not precise. I wanna put them on a scale of zero to one where zero is not much and, this thing did it to me again. So they fit into that thing. Zero is minimal. One is maximal. They're far from independent. So just because I have five doesn't mean that they're independent variables. They are not independent variables. Now, it would've been nice if I could've come up with an experiment that I could vary each one independently in just one experiment. That would've been nice, but that didn't happen. So anyway, density is the number of allocation, deallocation ops over the number of operations. And so zero means we don't use an allocator, and one means every instruction is allocating or deallocating. (audience laughs) So, if you think about populating a vector with push back, so what happens there? Think about what happens. If I just say push back, push back, push back on a vector to a billion, does that have a high allocation or a low allocation? And I'm specifically saying vector of int. Would you say that has a high allocation density or a low allocation density, where I push back a billion integers? What? - [Man in Audience] It's relatively low. - Relatively low. What do you guys think, low? - [Man in Audience] A little bit low. - Yes, sir. - [Man in Audience] Low. - We're all low. Right, because if I push a billion things on, there are gonna be 30 allocations, let's say, and then there are a billion other things, so 30 over a billion, small. Just giving you an idea. Then variation in allocated sizes. So everything is allocated the same. And then everything is different. Make sense? Okay. So consider like STD set of int or STD string. Just think about those guys. Do they have variation or are they all the same? If you have a set of int, every node is the same. If you have strings, it could vary all over the place, right? The footprint of the string is the same, but if it's a long enough string, then it could vary to whatever it is. Now locality is the most complex one. Locality, you have to think about the number of instructions in a sub-region over a duration, and then you have the memory size of the sub-region, and you have the number of transitions out of that during this period. And so if you look at those, they form this little, first order equation. I don't mean this to be really linear, but to a first order or a zeroth order, this is the idea. And then you can break into physical locality and forget about the T. You can break it into temporal locality and forget about the M. And again, I'm just trying to give you the idea that the more instructions, the more locality, and the smaller the memory, the more locality, and the fewer times you jump out of that region, the more locality. I'm just giving you the rough way the thing fits together. So, zero is, we've got a large and not accessed repeatedly. That's what most programs are, large and not accessed repeatedly. I mean, large or not accessed repeatedly, right? It's large. The region is the whole memory. Or it's small, but it goes all over the place. The other one is saying that the sub-region is small and accessed repeatedly. If I've got a small region, not all of memory but it has a little bit of memory, and I'm pounding on it, that's high locality. That's good. Okay. Even if we don't allocate a lot in our program, even if we just do a little bit of allocation and then spend the next seven days accessing memory, the performance improvement of local allocators can be amazing. I just want you to understand that it's not about the speed of allocation. That's another myth, that, "Oh, I need to allocate really quickly." No, you don't. You need to allocate well. Okay. Utilization, the max memory in use divided by the total memory allocated. If I allocate a byte and free it for the entire duration of the program, my utilization is very low. If I allocate everything up front, use it, and blow it away, it's very high. Does that make sense? We're good? Finally, again, consider vector of int and assume that we allocate the capacity up front. If I allocate the capacity of a vector up front, I got one piece. And whatever I do to it, nothing else is getting allocated. It's done, and then I free it. So that's everything up front. Whereas a vector of large strings, if I were to push and pop and push and pop and push and pop, I wouldn't be getting all the memory up front. I'd be getting some memory. Then I push and I pop and then I get some more memory, so the amount of memory that I've allocated is much larger than the amount of memory I have at any given point in time. And finally, there's contention. And contention really doesn't fit this, but you like to try to fit things into something, so this is the fifth category. If I have no contention at all, then it's zero. And if every thread is constantly trying to access the thing, or I should say is trying to allocate or deallocate memory, then I have maximal contention. All right. So we have this summary here. And if you notice, the mnemonic is going to be DVLUCK the duck. So I hope you remember it. Density, variation, locality, utilization, and contention. DVLUC. I'll make it even easier later. Okay, so remember that. So we're now at the next step, analyzing the benchmark data. And of course, this is what we were all waiting for, but we're almost out of time. I'm going to start, because I wanna make sure I have time for questions, but does anybody have a question right now? As long as it's not heckling. (audience laughs) I'm waiting. I'm saving the heckling for the end, 'cause I'm scared. Yes. - [Man in Audience] With monotonic allocators, how do you decide the size? - With allocators, how do I decide? - [Man in Audience] The size of the monotonic allocator? - The question is, with the monotonic allocator, how do I decide how big to make it? - [Man in Audience] On the stack. - On the stack. Okay. The way you decide is you figure out how big it needs to be, and you make it that big. (audience laughs) Yes. - [Man in Audience] If you use pragma pack would that also affect your allocators if you're using the previous allocator, the (mumbles) allocator where you're-- - Okay, the question is something about pragma-- - [Man in Audience] Pack. - Pack. I don't think I even know what that is. What is that? - [Man in Audience] Like naturally, would you align them back to back? - So the question is, if I use this thing that I've never heard of, will it make things-- (man in audience mumbles) I will tell you now, I have never heard of this thing, but if the question is, do I need a compiler pragma to do something, I think what you might be asking is, I think I might I know, I think I'm gonna figure it out with the question. So let me say what I think the question is. If I turn off alignment in the compiler so that everything just mushes together and I don't have to worry about any holes, is that right? - [Man in Audience] Yes. - Ah! Why didn't you say so? (audience laughs) The answer is your program will run like a dog with one leg, because that's not how computers work. That's how compilers work, but that's not how computers work. So we don't do that. Don't do that. Do not turn off alignment so that you get this packing. Yes. - [Man in Audience] I'm sorry. Just to clarify, if you are running x86 or x64, its performance impact is very, very small. - Okay. We just heard from somebody who knows. On an x86, the performance impact is very small, but not zero? Okay. - [Man in Audience] On anything else, it is completely (mumbles) - Okay. So what we heard is it's either not as good or horrific. (audience laughs) I'll take that. Anybody else? That's good to know, I guess. Yes. - [Man in Audience] So when you're measuring the locality, how do you determine the size of the memory region? 'Cause you said N was like the size of the memory region. - Okay, so the question is, how do you measure locality? And the answer is you don't. The idea is, this is a model for a human being to think about, what are the trade offs and so on? The idea is you measure various system sizes, and you say, "The system size is this big. "What happens if this big and so on?" And you compare, and it's like an accordion. You say, "I got one system that's big, "two systems that are like this, "four systems that are like this. "They're all the same size, but they're segmented." And you look and see what the surface looks like. And guess what we're gonna do? So that's happening. I want you to know that this talk did not happen in a couple of days. This talk is serious. We have about three minutes left. Maybe, I don't know if I should start, or if I should just break now and we'll come back maybe three minutes earlier. Let's start on time, 'cause we literally have two minutes or something left. Does anybody have anything else they wanna ask? Yes. - [Man in Audience] Is there anything about the monotonic allocator that says its buffer has to be on the stack? - Question is, does the monotonic allocator say anything about where the buffer comes from? The answer is no. You can specify whatever you want. Note to self, stack is hot. Yes. - [Man in Audience] Is there a such thing as a backup strategy for a monotonic allocator? - Is there a backup strategy for a monotonic allocator? Typically, it is the backup strategy, but if you wanted to put something that measured how much memory was going through, like a monitoring allocator, you can chain them. Anything else? We are gonna start sharp in 15 minutes, 'cause I'm going to get through the rest of this. The good stuff is now about to happen, some crazy good stuff. So go up, but don't be late.
Info
Channel: CppCon
Views: 44,918
Rating: undefined out of 5
Keywords: John Lakos, CppCon 2017, Computer Science (Field), + C (Programming Language), Bash Films, conference video recording services, conference recording services, nationwide conference recording services, conference videography services, conference video recording, conference filming services, conference services, conference recording, conference live streaming, event videographers, capture presentation slides, record presentation slides, event video recording
Id: nZNd5FjSquk
Channel Id: undefined
Length: 60min 5sec (3605 seconds)
Published: Sat Oct 14 2017
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.