Skip to content

Commit 628522e

Browse files
committed
sha1-lookup: more memory efficient search in sorted list of SHA-1
Currently, when looking for a packed object from the pack idx, a simple binary search is used. A conventional binary search loop looks like this: unsigned lo, hi; do { unsigned mi = (lo + hi) / 2; int cmp = "entry pointed at by mi" minus "target"; if (!cmp) return mi; "mi is the wanted one" if (cmp > 0) hi = mi; "mi is larger than target" else lo = mi+1; "mi is smaller than target" } while (lo < hi); "did not find what we wanted" The invariants are: - When entering the loop, 'lo' points at a slot that is never above the target (it could be at the target), 'hi' points at a slot that is guaranteed to be above the target (it can never be at the target). - We find a point 'mi' between 'lo' and 'hi' ('mi' could be the same as 'lo', but never can be as high as 'hi'), and check if 'mi' hits the target. There are three cases: - if it is a hit, we have found what we are looking for; - if it is strictly higher than the target, we set it to 'hi', and repeat the search. - if it is strictly lower than the target, we update 'lo' to one slot after it, because we allow 'lo' to be at the target and 'mi' is known to be below the target. If the loop exits, there is no matching entry. When choosing 'mi', we do not have to take the "middle" but anywhere in between 'lo' and 'hi', as long as lo <= mi < hi is satisfied. When we somehow know that the distance between the target and 'lo' is much shorter than the target and 'hi', we could pick 'mi' that is much closer to 'lo' than (hi+lo)/2, which a conventional binary search would pick. This patch takes advantage of the fact that the SHA-1 is a good hash function, and as long as there are enough entries in the table, we can expect uniform distribution. An entry that begins with for example "deadbeef..." is much likely to appear much later than in the midway of a reasonably populated table. In fact, it can be expected to be near 87% (222/256) from the top of the table. This is a work-in-progress and has switches to allow easier experiments and debugging. Exporting GIT_USE_LOOKUP environment variable enables this code. On my admittedly memory starved machine, with a partial KDE repository (3.0G pack with 95M idx): $ GIT_USE_LOOKUP=t git log -800 --stat HEAD >/dev/null 3.93user 0.16system 0:04.09elapsed 100%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+55588minor)pagefaults 0swaps Without the patch, the numbers are: $ git log -800 --stat HEAD >/dev/null 4.00user 0.15system 0:04.17elapsed 99%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+60258minor)pagefaults 0swaps In the same repository: $ GIT_USE_LOOKUP=t git log -2000 HEAD >/dev/null 0.12user 0.00system 0:00.12elapsed 97%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+4241minor)pagefaults 0swaps Without the patch, the numbers are: $ git log -2000 HEAD >/dev/null 0.05user 0.01system 0:00.07elapsed 100%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+8506minor)pagefaults 0swaps There isn't much time difference, but the number of minor faults seems to show that we are touching much smaller number of pages, which is expected. Signed-off-by: Junio C Hamano <[email protected]>
1 parent 2a5fe25 commit 628522e

File tree

4 files changed

+195
-3
lines changed

4 files changed

+195
-3
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ LIB_H += refs.h
366366
LIB_H += remote.h
367367
LIB_H += revision.h
368368
LIB_H += run-command.h
369+
LIB_H += sha1-lookup.h
369370
LIB_H += sideband.h
370371
LIB_H += strbuf.h
371372
LIB_H += tag.h
@@ -446,6 +447,7 @@ LIB_OBJS += run-command.o
446447
LIB_OBJS += server-info.o
447448
LIB_OBJS += setup.o
448449
LIB_OBJS += sha1_file.o
450+
LIB_OBJS += sha1-lookup.o
449451
LIB_OBJS += sha1_name.o
450452
LIB_OBJS += shallow.o
451453
LIB_OBJS += sideband.o

sha1-lookup.c

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#include "cache.h"
2+
#include "sha1-lookup.h"
3+
4+
/*
5+
* Conventional binary search loop looks like this:
6+
*
7+
* unsigned lo, hi;
8+
* do {
9+
* unsigned mi = (lo + hi) / 2;
10+
* int cmp = "entry pointed at by mi" minus "target";
11+
* if (!cmp)
12+
* return (mi is the wanted one)
13+
* if (cmp > 0)
14+
* hi = mi; "mi is larger than target"
15+
* else
16+
* lo = mi+1; "mi is smaller than target"
17+
* } while (lo < hi);
18+
*
19+
* The invariants are:
20+
*
21+
* - When entering the loop, lo points at a slot that is never
22+
* above the target (it could be at the target), hi points at a
23+
* slot that is guaranteed to be above the target (it can never
24+
* be at the target).
25+
*
26+
* - We find a point 'mi' between lo and hi (mi could be the same
27+
* as lo, but never can be as same as hi), and check if it hits
28+
* the target. There are three cases:
29+
*
30+
* - if it is a hit, we are happy.
31+
*
32+
* - if it is strictly higher than the target, we set it to hi,
33+
* and repeat the search.
34+
*
35+
* - if it is strictly lower than the target, we update lo to
36+
* one slot after it, because we allow lo to be at the target.
37+
*
38+
* If the loop exits, there is no matching entry.
39+
*
40+
* When choosing 'mi', we do not have to take the "middle" but
41+
* anywhere in between lo and hi, as long as lo <= mi < hi is
42+
* satisfied. When we somehow know that the distance between the
43+
* target and lo is much shorter than the target and hi, we could
44+
* pick mi that is much closer to lo than the midway.
45+
*
46+
* Now, we can take advantage of the fact that SHA-1 is a good hash
47+
* function, and as long as there are enough entries in the table, we
48+
* can expect uniform distribution. An entry that begins with for
49+
* example "deadbeef..." is much likely to appear much later than in
50+
* the midway of the table. It can reasonably be expected to be near
51+
* 87% (222/256) from the top of the table.
52+
*
53+
* The table at "table" holds at least "nr" entries of "elem_size"
54+
* bytes each. Each entry has the SHA-1 key at "key_offset". The
55+
* table is sorted by the SHA-1 key of the entries. The caller wants
56+
* to find the entry with "key", and knows that the entry at "lo" is
57+
* not higher than the entry it is looking for, and that the entry at
58+
* "hi" is higher than the entry it is looking for.
59+
*/
60+
int sha1_entry_pos(const void *table,
61+
size_t elem_size,
62+
size_t key_offset,
63+
unsigned lo, unsigned hi, unsigned nr,
64+
const unsigned char *key)
65+
{
66+
const unsigned char *base = table;
67+
const unsigned char *hi_key, *lo_key;
68+
unsigned ofs_0;
69+
static int debug_lookup = -1;
70+
71+
if (debug_lookup < 0)
72+
debug_lookup = !!getenv("GIT_DEBUG_LOOKUP");
73+
74+
if (!nr || lo >= hi)
75+
return -1;
76+
77+
if (nr == hi)
78+
hi_key = NULL;
79+
else
80+
hi_key = base + elem_size * hi + key_offset;
81+
lo_key = base + elem_size * lo + key_offset;
82+
83+
ofs_0 = 0;
84+
do {
85+
int cmp;
86+
unsigned ofs, mi, range;
87+
unsigned lov, hiv, kyv;
88+
const unsigned char *mi_key;
89+
90+
range = hi - lo;
91+
if (hi_key) {
92+
for (ofs = ofs_0; ofs < 20; ofs++)
93+
if (lo_key[ofs] != hi_key[ofs])
94+
break;
95+
ofs_0 = ofs;
96+
/*
97+
* byte 0 thru (ofs-1) are the same between
98+
* lo and hi; ofs is the first byte that is
99+
* different.
100+
*/
101+
hiv = hi_key[ofs_0];
102+
if (ofs_0 < 19)
103+
hiv = (hiv << 8) | hi_key[ofs_0+1];
104+
} else {
105+
hiv = 256;
106+
if (ofs_0 < 19)
107+
hiv <<= 8;
108+
}
109+
lov = lo_key[ofs_0];
110+
kyv = key[ofs_0];
111+
if (ofs_0 < 19) {
112+
lov = (lov << 8) | lo_key[ofs_0+1];
113+
kyv = (kyv << 8) | key[ofs_0+1];
114+
}
115+
assert(lov < hiv);
116+
117+
if (kyv < lov)
118+
return -1 - lo;
119+
if (hiv < kyv)
120+
return -1 - hi;
121+
122+
if (kyv == lov && lov < hiv - 1)
123+
kyv++;
124+
else if (kyv == hiv - 1 && lov < kyv)
125+
kyv--;
126+
127+
mi = (range - 1) * (kyv - lov) / (hiv - lov) + lo;
128+
129+
if (debug_lookup) {
130+
printf("lo %u hi %u rg %u mi %u ", lo, hi, range, mi);
131+
printf("ofs %u lov %x, hiv %x, kyv %x\n",
132+
ofs_0, lov, hiv, kyv);
133+
}
134+
if (!(lo <= mi && mi < hi))
135+
die("assertion failure lo %u mi %u hi %u %s",
136+
lo, mi, hi, sha1_to_hex(key));
137+
138+
mi_key = base + elem_size * mi + key_offset;
139+
cmp = memcmp(mi_key + ofs_0, key + ofs_0, 20 - ofs_0);
140+
if (!cmp)
141+
return mi;
142+
if (cmp > 0) {
143+
hi = mi;
144+
hi_key = mi_key;
145+
}
146+
else {
147+
lo = mi + 1;
148+
lo_key = mi_key + elem_size;
149+
}
150+
} while (lo < hi);
151+
return -lo-1;
152+
}

sha1-lookup.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#ifndef SHA1_LOOKUP_H
2+
#define SHA1_LOOKUP_H
3+
4+
extern int sha1_entry_pos(const void *table,
5+
size_t elem_size,
6+
size_t key_offset,
7+
unsigned lo, unsigned hi, unsigned nr,
8+
const unsigned char *key);
9+
#endif

sha1_file.c

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "tree.h"
1616
#include "refs.h"
1717
#include "pack-revindex.h"
18+
#include "sha1-lookup.h"
1819

1920
#ifndef O_NOATIME
2021
#if defined(__linux__) && (defined(__i386__) || defined(__PPC__))
@@ -1675,7 +1676,12 @@ off_t find_pack_entry_one(const unsigned char *sha1,
16751676
{
16761677
const uint32_t *level1_ofs = p->index_data;
16771678
const unsigned char *index = p->index_data;
1678-
unsigned hi, lo;
1679+
unsigned hi, lo, stride;
1680+
static int use_lookup = -1;
1681+
static int debug_lookup = -1;
1682+
1683+
if (debug_lookup < 0)
1684+
debug_lookup = !!getenv("GIT_DEBUG_LOOKUP");
16791685

16801686
if (!index) {
16811687
if (open_pack_index(p))
@@ -1690,11 +1696,34 @@ off_t find_pack_entry_one(const unsigned char *sha1,
16901696
index += 4 * 256;
16911697
hi = ntohl(level1_ofs[*sha1]);
16921698
lo = ((*sha1 == 0x0) ? 0 : ntohl(level1_ofs[*sha1 - 1]));
1699+
if (p->index_version > 1) {
1700+
stride = 20;
1701+
} else {
1702+
stride = 24;
1703+
index += 4;
1704+
}
1705+
1706+
if (debug_lookup)
1707+
printf("%02x%02x%02x... lo %u hi %u nr %u\n",
1708+
sha1[0], sha1[1], sha1[2], lo, hi, p->num_objects);
1709+
1710+
if (use_lookup < 0)
1711+
use_lookup = !!getenv("GIT_USE_LOOKUP");
1712+
if (use_lookup) {
1713+
int pos = sha1_entry_pos(index, stride, 0,
1714+
lo, hi, p->num_objects, sha1);
1715+
if (pos < 0)
1716+
return 0;
1717+
return nth_packed_object_offset(p, pos);
1718+
}
16931719

16941720
do {
16951721
unsigned mi = (lo + hi) / 2;
1696-
unsigned x = (p->index_version > 1) ? (mi * 20) : (mi * 24 + 4);
1697-
int cmp = hashcmp(index + x, sha1);
1722+
int cmp = hashcmp(index + mi * stride, sha1);
1723+
1724+
if (debug_lookup)
1725+
printf("lo %u hi %u rg %u mi %u\n",
1726+
lo, hi, hi - lo, mi);
16981727
if (!cmp)
16991728
return nth_packed_object_offset(p, mi);
17001729
if (cmp > 0)

0 commit comments

Comments
 (0)