| Concerning Git's Packing Heuristics | |
| =================================== | |
| Oh, here's a really stupid question: | |
| Where do I go | |
| to learn the details | |
| of Git's packing heuristics? | |
| Be careful what you ask! | |
| Followers of the Git, please open the Git IRC Log and turn to | |
| February 10, 2006. | |
| It's a rare occasion, and we are joined by the King Git Himself, | |
| Linus Torvalds (linus). Nathaniel Smith, (njs`), has the floor | |
| and seeks enlightenment. Others are present, but silent. | |
| Let's listen in! | |
| <njs`> Oh, here's a really stupid question -- where do I go to | |
| learn the details of Git's packing heuristics? google avails | |
| me not, reading the source didn't help a lot, and wading | |
| through the whole mailing list seems less efficient than any | |
| of that. | |
| It is a bold start! A plea for help combined with a simultaneous | |
| tri-part attack on some of the tried and true mainstays in the quest | |
| for enlightenment. Brash accusations of google being useless. Hubris! | |
| Maligning the source. Heresy! Disdain for the mailing list archives. | |
| Woe. | |
| <pasky> yes, the packing-related delta stuff is somewhat | |
| mysterious even for me ;) | |
| Ah! Modesty after all. | |
| <linus> njs, I don't think the docs exist. That's something where | |
| I don't think anybody else than me even really got involved. | |
| Most of the rest of Git others have been busy with (especially | |
| Junio), but packing nobody touched after I did it. | |
| It's cryptic, yet vague. Linus in style for sure. Wise men | |
| interpret this as an apology. A few argue it is merely a | |
| statement of fact. | |
| <njs`> I guess the next step is "read the source again", but I | |
| have to build up a certain level of gumption first :-) | |
| Indeed! On both points. | |
| <linus> The packing heuristic is actually really really simple. | |
| Bait... | |
| <linus> But strange. | |
| And switch. That ought to do it! | |
| <linus> Remember: Git really doesn't follow files. So what it does is | |
| - generate a list of all objects | |
| - sort the list according to magic heuristics | |
| - walk the list, using a sliding window, seeing if an object | |
| can be diffed against another object in the window | |
| - write out the list in recency order | |
| The traditional understatement: | |
| <njs`> I suspect that what I'm missing is the precise definition of | |
| the word "magic" | |
| The traditional insight: | |
| <pasky> yes | |
| And Babel-like confusion flowed. | |
| <njs`> oh, hmm, and I'm not sure what this sliding window means either | |
| <pasky> iirc, it appeared to me to be just the sha1 of the object | |
| when reading the code casually ... | |
| ... which simply doesn't sound as a very good heuristics, though ;) | |
| <njs`> .....and recency order. okay, I think it's clear I didn't | |
| even realize how much I wasn't realizing :-) | |
| Ah, grasshopper! And thus the enlightenment begins anew. | |
| <linus> The "magic" is actually in theory totally arbitrary. | |
| ANY order will give you a working pack, but no, it's not | |
| ordered by SHA-1. | |
| Before talking about the ordering for the sliding delta | |
| window, let's talk about the recency order. That's more | |
| important in one way. | |
| <njs`> Right, but if all you want is a working way to pack things | |
| together, you could just use cat and save yourself some | |
| trouble... | |
| Waaait for it.... | |
| <linus> The recency ordering (which is basically: put objects | |
| _physically_ into the pack in the order that they are | |
| "reachable" from the head) is important. | |
| <njs`> okay | |
| <linus> It's important because that's the thing that gives packs | |
| good locality. It keeps the objects close to the head (whether | |
| they are old or new, but they are _reachable_ from the head) | |
| at the head of the pack. So packs actually have absolutely | |
| _wonderful_ IO patterns. | |
| Read that again, because it is important. | |
| <linus> But recency ordering is totally useless for deciding how | |
| to actually generate the deltas, so the delta ordering is | |
| something else. | |
| The delta ordering is (wait for it): | |
| - first sort by the "basename" of the object, as defined by | |
| the name the object was _first_ reached through when | |
| generating the object list | |
| - within the same basename, sort by size of the object | |
| - but always sort different types separately (commits first). | |
| That's not exactly it, but it's very close. | |
| <njs`> The "_first_ reached" thing is not too important, just you | |
| need some way to break ties since the same objects may be | |
| reachable many ways, yes? | |
| And as if to clarify: | |
| <linus> The point is that it's all really just any random | |
| heuristic, and the ordering is totally unimportant for | |
| correctness, but it helps a lot if the heuristic gives | |
| "clumping" for things that are likely to delta well against | |
| each other. | |
| It is an important point, so secretly, I did my own research and have | |
| included my results below. To be fair, it has changed some over time. | |
| And through the magic of Revisionistic History, I draw upon this entry | |
| from The Git IRC Logs on my father's birthday, March 1: | |
| <gitster> The quote from the above linus should be rewritten a | |
| bit (wait for it): | |
| - first sort by type. Different objects never delta with | |
| each other. | |
| - then sort by filename/dirname. hash of the basename | |
| occupies the top BITS_PER_INT-DIR_BITS bits, and bottom | |
| DIR_BITS are for the hash of leading path elements. | |
| - then if we are doing "thin" pack, the objects we are _not_ | |
| going to pack but we know about are sorted earlier than | |
| other objects. | |
| - and finally sort by size, larger to smaller. | |
| In one swell-foop, clarification and obscurification! Nonetheless, | |
| authoritative. Cryptic, yet concise. It even solicits notions of | |
| quotes from The Source Code. Clearly, more study is needed. | |
| <gitster> That's the sort order. What this means is: | |
| - we do not delta different object types. | |
| - we prefer to delta the objects with the same full path, but | |
| allow files with the same name from different directories. | |
| - we always prefer to delta against objects we are not going | |
| to send, if there are some. | |
| - we prefer to delta against larger objects, so that we have | |
| lots of removals. | |
| The penultimate rule is for "thin" packs. It is used when | |
| the other side is known to have such objects. | |
| There it is again. "Thin" packs. I'm thinking to myself, "What | |
| is a 'thin' pack?" So I ask: | |
| <jdl> What is a "thin" pack? | |
| <gitster> Use of --objects-edge to rev-list as the upstream of | |
| pack-objects. The pack transfer protocol negotiates that. | |
| Woo hoo! Cleared that _right_ up! | |
| <gitster> There are two directions - push and fetch. | |
| There! Did you see it? It is not '"push" and "pull"'! How often the | |
| confusion has started here. So casually mentioned, too! | |
| <gitster> For push, git-send-pack invokes git-receive-pack on the | |
| other end. The receive-pack says "I have up to these commits". | |
| send-pack looks at them, and computes what are missing from | |
| the other end. So "thin" could be the default there. | |
| In the other direction, fetch, git-fetch-pack and | |
| git-clone-pack invokes git-upload-pack on the other end | |
| (via ssh or by talking to the daemon). | |
| There are two cases: fetch-pack with -k and clone-pack is one, | |
| fetch-pack without -k is the other. clone-pack and fetch-pack | |
| with -k will keep the downloaded packfile without expanded, so | |
| we do not use thin pack transfer. Otherwise, the generated | |
| pack will have delta without base object in the same pack. | |
| But fetch-pack without -k will explode the received pack into | |
| individual objects, so we automatically ask upload-pack to | |
| give us a thin pack if upload-pack supports it. | |
| OK then. | |
| Uh. | |
| Let's return to the previous conversation still in progress. | |
| <njs`> and "basename" means something like "the tail of end of | |
| path of file objects and dir objects, as per basename(3), and | |
| we just declare all commit and tag objects to have the same | |
| basename" or something? | |
| Luckily, that too is a point that gitster clarified for us! | |
| If I might add, the trick is to make files that _might_ be similar be | |
| located close to each other in the hash buckets based on their file | |
| names. It used to be that "foo/Makefile", "bar/baz/quux/Makefile" and | |
| "Makefile" all landed in the same bucket due to their common basename, | |
| "Makefile". However, now they land in "close" buckets. | |
| The algorithm allows not just for the _same_ bucket, but for _close_ | |
| buckets to be considered delta candidates. The rationale is | |
| essentially that files, like Makefiles, often have very similar | |
| content no matter what directory they live in. | |
| <linus> I played around with different delta algorithms, and with | |
| making the "delta window" bigger, but having too big of a | |
| sliding window makes it very expensive to generate the pack: | |
| you need to compare every object with a _ton_ of other objects. | |
| There are a number of other trivial heuristics too, which | |
| basically boil down to "don't bother even trying to delta this | |
| pair" if we can tell before-hand that the delta isn't worth it | |
| (due to size differences, where we can take a previous delta | |
| result into account to decide that "ok, no point in trying | |
| that one, it will be worse"). | |
| End result: packing is actually very size efficient. It's | |
| somewhat CPU-wasteful, but on the other hand, since you're | |
| really only supposed to do it maybe once a month (and you can | |
| do it during the night), nobody really seems to care. | |
| Nice Engineering Touch, there. Find when it doesn't matter, and | |
| proclaim it a non-issue. Good style too! | |
| <njs`> So, just to repeat to see if I'm following, we start by | |
| getting a list of the objects we want to pack, we sort it by | |
| this heuristic (basically lexicographically on the tuple | |
| (type, basename, size)). | |
| Then we walk through this list, and calculate a delta of | |
| each object against the last n (tunable parameter) objects, | |
| and pick the smallest of these deltas. | |
| Vastly simplified, but the essence is there! | |
| <linus> Correct. | |
| <njs`> And then once we have picked a delta or fulltext to | |
| represent each object, we re-sort by recency, and write them | |
| out in that order. | |
| <linus> Yup. Some other small details: | |
| And of course there is the "Other Shoe" Factor too. | |
| <linus> - We limit the delta depth to another magic value (right | |
| now both the window and delta depth magic values are just "10") | |
| <njs`> Hrm, my intuition is that you'd end up with really _bad_ IO | |
| patterns, because the things you want are near by, but to | |
| actually reconstruct them you may have to jump all over in | |
| random ways. | |
| <linus> - When we write out a delta, and we haven't yet written | |
| out the object it is a delta against, we write out the base | |
| object first. And no, when we reconstruct them, we actually | |
| get nice IO patterns, because: | |
| - larger objects tend to be "more recent" (Linus' law: files grow) | |
| - we actively try to generate deltas from a larger object to a | |
| smaller one | |
| - this means that the top-of-tree very seldom has deltas | |
| (i.e. deltas in _practice_ are "backwards deltas") | |
| Again, we should reread that whole paragraph. Not just because | |
| Linus has slipped Linus's Law in there on us, but because it is | |
| important. Let's make sure we clarify some of the points here: | |
| <njs`> So the point is just that in practice, delta order and | |
| recency order match each other quite well. | |
| <linus> Yes. There's another nice side to this (and yes, it was | |
| designed that way ;): | |
| - the reason we generate deltas against the larger object is | |
| actually a big space saver too! | |
| <njs`> Hmm, but your last comment (if "we haven't yet written out | |
| the object it is a delta against, we write out the base object | |
| first"), seems like it would make these facts mostly | |
| irrelevant because even if in practice you would not have to | |
| wander around much, in fact you just brute-force say that in | |
| the cases where you might have to wander, don't do that :-) | |
| <linus> Yes and no. Notice the rule: we only write out the base | |
| object first if the delta against it was more recent. That | |
| means that you can actually have deltas that refer to a base | |
| object that is _not_ close to the delta object, but that only | |
| happens when the delta is needed to generate an _old_ object. | |
| <linus> See? | |
| Yeah, no. I missed that on the first two or three readings myself. | |
| <linus> This keeps the front of the pack dense. The front of the | |
| pack never contains data that isn't relevant to a "recent" | |
| object. The size optimization comes from our use of xdelta | |
| (but is true for many other delta algorithms): removing data | |
| is cheaper (in size) than adding data. | |
| When you remove data, you only need to say "copy bytes n--m". | |
| In contrast, in a delta that _adds_ data, you have to say "add | |
| these bytes: 'actual data goes here'" | |
| *** njs` has quit: Read error: 104 (Connection reset by peer) | |
| <linus> Uhhuh. I hope I didn't blow njs` mind. | |
| *** njs` has joined channel #git | |
| <pasky> :) | |
| The silent observers are amused. Of course. | |
| And as if njs` was expected to be omniscient: | |
| <linus> njs - did you miss anything? | |
| OK, I'll spell it out. That's Geek Humor. If njs` was not actually | |
| connected for a little bit there, how would he know if missed anything | |
| while he was disconnected? He's a benevolent dictator with a sense of | |
| humor! Well noted! | |
| <njs`> Stupid router. Or gremlins, or whatever. | |
| It's a cheap shot at Cisco. Take 'em when you can. | |
| <njs`> Yes and no. Notice the rule: we only write out the base | |
| object first if the delta against it was more recent. | |
| I'm getting lost in all these orders, let me re-read :-) | |
| So the write-out order is from most recent to least recent? | |
| (Conceivably it could be the opposite way too, I'm not sure if | |
| we've said) though my connection back at home is logging, so I | |
| can just read what you said there :-) | |
| And for those of you paying attention, the Omniscient Trick has just | |
| been detailed! | |
| <linus> Yes, we always write out most recent first | |
| <njs`> And, yeah, I got the part about deeper-in-history stuff | |
| having worse IO characteristics, one sort of doesn't care. | |
| <linus> With the caveat that if the "most recent" needs an older | |
| object to delta against (hey, shrinking sometimes does | |
| happen), we write out the old object with the delta. | |
| <njs`> (if only it happened more...) | |
| <linus> Anyway, the pack-file could easily be denser still, but | |
| because it's used both for streaming (the Git protocol) and | |
| for on-disk, it has a few pessimizations. | |
| Actually, it is a made-up word. But it is a made-up word being | |
| used as setup for a later optimization, which is a real word: | |
| <linus> In particular, while the pack-file is then compressed, | |
| it's compressed just one object at a time, so the actual | |
| compression factor is less than it could be in theory. But it | |
| means that it's all nice random-access with a simple index to | |
| do "object name->location in packfile" translation. | |
| <njs`> I'm assuming the real win for delta-ing large->small is | |
| more homogeneous statistics for gzip to run over? | |
| (You have to put the bytes in one place or another, but | |
| putting them in a larger blob wins on compression) | |
| Actually, what is the compression strategy -- each delta | |
| individually gzipped, the whole file gzipped, somewhere in | |
| between, no compression at all, ....? | |
| Right. | |
| Reality IRC sets in. For example: | |
| <pasky> I'll read the rest in the morning, I really have to go | |
| sleep or there's no hope whatsoever for me at the today's | |
| exam... g'nite all. | |
| Heh. | |
| <linus> pasky: g'nite | |
| <njs`> pasky: 'luck | |
| <linus> Right: large->small matters exactly because of compression | |
| behaviour. If it was non-compressed, it probably wouldn't make | |
| any difference. | |
| <njs`> yeah | |
| <linus> Anyway: I'm not even trying to claim that the pack-files | |
| are perfect, but they do tend to have a nice balance of | |
| density vs ease-of use. | |
| Gasp! OK, saved. That's a fair Engineering trade off. Close call! | |
| In fact, Linus reflects on some Basic Engineering Fundamentals, | |
| design options, etc. | |
| <linus> More importantly, they allow Git to still _conceptually_ | |
| never deal with deltas at all, and be a "whole object" store. | |
| Which has some problems (we discussed bad huge-file | |
| behaviour on the Git lists the other day), but it does mean | |
| that the basic Git concepts are really really simple and | |
| straightforward. | |
| It's all been quite stable. | |
| Which I think is very much a result of having very simple | |
| basic ideas, so that there's never any confusion about what's | |
| going on. | |
| Bugs happen, but they are "simple" bugs. And bugs that | |
| actually get some object store detail wrong are almost always | |
| so obvious that they never go anywhere. | |
| <njs`> Yeah. | |
| Nuff said. | |
| <linus> Anyway. I'm off for bed. It's not 6AM here, but I've got | |
| three kids, and have to get up early in the morning to send | |
| them off. I need my beauty sleep. | |
| <njs`> :-) | |
| <njs`> appreciate the infodump, I really was failing to find the | |
| details on Git packs :-) | |
| And now you know the rest of the story. |