Skip to content

Commit 0b84c95

Browse files
author
Harshil Goel
authored
perf(query): Update CompressedBin IntersectionAlgo (#9000)
Updated algo to intersect. Getting upto 175% improvment overall ``` goos: linux goarch: amd64 pkg: github.com/dgraph-io/dgraph/algo cpu: 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz │ in1 │ main_in │ │ sec/op │ sec/op vs base │ ListIntersectCompressBin/compressed:IntersectWith:ratio=0.01:size=100:overlap=0.01:-8 91.21n ± ∞ ¹ 387.70n ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=0.1:size=100:overlap=0.01:-8 336.0n ± ∞ ¹ 1733.0n ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=1:size=100:overlap=0.01:-8 1.093µ ± ∞ ¹ 3.481µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=10:size=100:overlap=0.01:-8 4.719µ ± ∞ ¹ 8.600µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=100:size=100:overlap=0.01:-8 19.76µ ± ∞ ¹ 44.32µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=0.01:size=1000:overlap=0.01:-8 481.7n ± ∞ ¹ 784.2n ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=0.1:size=1000:overlap=0.01:-8 1.502µ ± ∞ ¹ 3.667µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=1:size=1000:overlap=0.01:-8 5.454µ ± ∞ ¹ 36.977µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=10:size=1000:overlap=0.01:-8 33.14µ ± ∞ ¹ 70.73µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=100:size=1000:overlap=0.01:-8 179.8µ ± ∞ ¹ 485.1µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=0.01:size=10000:overlap=0.01:-8 3.214µ ± ∞ ¹ 4.459µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=0.1:size=10000:overlap=0.01:-8 12.00µ ± ∞ ¹ 32.78µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=1:size=10000:overlap=0.01:-8 76.90µ ± ∞ ¹ 356.05µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=10:size=10000:overlap=0.01:-8 371.6µ ± ∞ ¹ 923.1µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=100:size=10000:overlap=0.01:-8 1.894m ± ∞ ¹ 5.141m ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=0.01:size=100000:overlap=0.01:-8 48.13µ ± ∞ ¹ 60.59µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=0.1:size=100000:overlap=0.01:-8 146.7µ ± ∞ ¹ 611.9µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=1:size=100000:overlap=0.01:-8 943.7µ ± ∞ ¹ 3359.6µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=10:size=100000:overlap=0.01:-8 3.926m ± ∞ ¹ 8.849m ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=100:size=100000:overlap=0.01:-8 20.78m ± ∞ ¹ ListIntersectCompressBin/compressed:IntersectWith:ratio=0.01:size=1000000:overlap=0.01:-8 862.4µ ± ∞ ¹ 1142.5µ ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=0.1:size=1000000:overlap=0.01:-8 1.599m ± ∞ ¹ 7.878m ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=1:size=1000000:overlap=0.01:-8 9.791m ± ∞ ¹ 33.871m ± ∞ ¹ ~ (p=1.000 n=1) ² ListIntersectCompressBin/compressed:IntersectWith:ratio=10:size=1000000:overlap=0.01:-8 44.40m ± ∞ ¹ geomean 66.19µ 104.6µ +175.93% ³ ```
1 parent 6dc93c7 commit 0b84c95

File tree

3 files changed

+139
-34
lines changed

3 files changed

+139
-34
lines changed

algo/uidlist.go

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import (
2424
"github.com/dgraph-io/dgraph/protos/pb"
2525
)
2626

27-
const jump = 32 // Jump size in InsersectWithJump.
27+
const jump = 32 // Jump size in InsersectWithJump.
28+
const linVsBinRatio = 10 // When is linear search better than binary
2829

2930
// ApplyFilter applies a filter to our UIDList.
3031
func ApplyFilter(u *pb.List, f func(uint64, int) bool) {
@@ -60,7 +61,7 @@ func IntersectCompressedWith(pack *pb.UidPack, afterUID uint64, v, o *pb.List) {
6061

6162
// Select appropriate function based on heuristics.
6263
ratio := float64(m) / float64(n)
63-
if ratio < 500 {
64+
if ratio < linVsBinRatio {
6465
IntersectCompressedWithLinJump(&dec, v.Uids, &dst)
6566
} else {
6667
IntersectCompressedWithBin(&dec, v.Uids, &dst)
@@ -94,7 +95,7 @@ func IntersectCompressedWithLinJump(dec *codec.Decoder, v []uint64, o *[]uint64)
9495
// https://link.springer.com/chapter/10.1007/978-3-642-12476-1_3
9596
// Call seek on dec before calling this function
9697
func IntersectCompressedWithBin(dec *codec.Decoder, q []uint64, o *[]uint64) {
97-
ld := dec.ApproxLen()
98+
ld := codec.ExactLen(dec.Pack)
9899
lq := len(q)
99100

100101
if lq == 0 {
@@ -105,46 +106,44 @@ func IntersectCompressedWithBin(dec *codec.Decoder, q []uint64, o *[]uint64) {
105106
}
106107

107108
// Pick the shorter list and do binary search
108-
if ld < lq {
109+
if ld <= lq {
109110
for {
110111
blockUids := dec.Uids()
111112
if len(blockUids) == 0 {
112113
break
113114
}
114-
IntersectWithBin(blockUids, q, o)
115-
lastUid := blockUids[len(blockUids)-1]
116-
qidx := sort.Search(len(q), func(idx int) bool {
117-
return q[idx] >= lastUid
118-
})
119-
if qidx >= len(q) {
115+
_, off := IntersectWithJump(blockUids, q, o)
116+
q = q[off:]
117+
if len(q) == 0 {
120118
return
121119
}
122-
q = q[qidx:]
123120
dec.Next()
124121
}
125122
return
126123
}
127124

128-
var uids []uint64
129-
for _, u := range q {
125+
uids := dec.Uids()
126+
qidx := 0
127+
for {
128+
if qidx >= len(q) {
129+
return
130+
}
131+
u := q[qidx]
130132
if len(uids) == 0 || u > uids[len(uids)-1] {
131-
uids = dec.Seek(u, codec.SeekStart)
133+
if lq*linVsBinRatio < ld {
134+
uids = dec.LinearSeek(u)
135+
} else {
136+
uids = dec.SeekToBlock(u, codec.SeekCurrent)
137+
}
132138
if len(uids) == 0 {
133139
return
134140
}
135141
}
136-
uidIdx := sort.Search(len(uids), func(idx int) bool {
137-
return uids[idx] >= u
138-
})
139-
if uidIdx >= len(uids) {
140-
// We know that u < max(uids). If we didn't find it here, it's not here.
141-
continue
142-
}
143-
if uids[uidIdx] == u {
144-
*o = append(*o, u)
145-
uidIdx++
142+
_, off := IntersectWithJump(uids, q[qidx:], o)
143+
if off == 0 {
144+
off = 1 // if v[k] isn't in u, move forward
146145
}
147-
uids = uids[uidIdx:]
146+
qidx += off
148147
}
149148
}
150149

@@ -233,7 +232,8 @@ func IntersectWithJump(u, v []uint64, o *[]uint64) (int, int) {
233232
// IntersectWithBin is based on the paper
234233
// "Fast Intersection Algorithms for Sorted Sequences"
235234
// https://link.springer.com/chapter/10.1007/978-3-642-12476-1_3
236-
func IntersectWithBin(d, q []uint64, o *[]uint64) {
235+
// Returns where to move the second array(q) to. O means not found
236+
func IntersectWithBin(d, q []uint64, o *[]uint64) int {
237237
ld := len(d)
238238
lq := len(q)
239239

@@ -242,7 +242,7 @@ func IntersectWithBin(d, q []uint64, o *[]uint64) {
242242
d, q = q, d
243243
}
244244
if ld == 0 || lq == 0 || d[ld-1] < q[0] || q[lq-1] < d[0] {
245-
return
245+
return 0
246246
}
247247

248248
val := d[0]
@@ -256,6 +256,7 @@ func IntersectWithBin(d, q []uint64, o *[]uint64) {
256256
})
257257

258258
binIntersect(d, q[minq:maxq], o)
259+
return maxq
259260
}
260261

261262
// binIntersect is the recursive function used.

algo/uidlist_test.go

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ func BenchmarkListIntersectCompressBin(b *testing.B) {
373373
for _, r := range rs {
374374
sz1 := sz
375375
sz2 := int(float64(sz) * r)
376-
if sz2 > 1000000 || sz2 == 0 {
376+
if sz2 > 10000000 || sz2 == 0 {
377377
break
378378
}
379379

@@ -389,8 +389,18 @@ func BenchmarkListIntersectCompressBin(b *testing.B) {
389389
sort.Slice(v1, func(i, j int) bool { return v1[i] < v1[j] })
390390

391391
dst2 := &pb.List{}
392+
dst1 := &pb.List{}
392393
compressedUids := codec.Encode(v1, 256)
393394

395+
b.Run(fmt.Sprintf("linJump:IntersectWith:ratio=%v:size=%d:overlap=%.2f:", r, sz, overlap),
396+
func(b *testing.B) {
397+
for k := 0; k < b.N; k++ {
398+
dec := codec.Decoder{Pack: compressedUids}
399+
dec.Seek(0, codec.SeekStart)
400+
IntersectCompressedWithLinJump(&dec, u1, &dst1.Uids)
401+
}
402+
})
403+
394404
b.Run(fmt.Sprintf("compressed:IntersectWith:ratio=%v:size=%d:overlap=%.2f:", r, sz, overlap),
395405
func(b *testing.B) {
396406
for k := 0; k < b.N; k++ {
@@ -399,7 +409,6 @@ func BenchmarkListIntersectCompressBin(b *testing.B) {
399409
IntersectCompressedWithBin(&dec, u1, &dst2.Uids)
400410
}
401411
})
402-
fmt.Println()
403412

404413
codec.FreePack(compressedUids)
405414
}
@@ -493,6 +502,43 @@ func sortUint64(nums []uint64) {
493502
sort.Slice(nums, func(i, j int) bool { return nums[i] < nums[j] })
494503
}
495504

505+
func fillNumsDiff(N1, N2, N3 int) ([]uint64, []uint64, []uint64) {
506+
rand.Seed(time.Now().UnixNano())
507+
508+
commonNums := make([]uint64, N1)
509+
blockNums := make([]uint64, N1+N2)
510+
otherNums := make([]uint64, N1+N3)
511+
allC := make(map[uint64]bool)
512+
513+
for i := 0; i < N1; i++ {
514+
val := rand.Uint64() % 1000
515+
commonNums[i] = val
516+
blockNums[i] = val
517+
otherNums[i] = val
518+
allC[val] = true
519+
}
520+
521+
for i := N1; i < N1+N2; i++ {
522+
val := rand.Uint64() % 1000
523+
blockNums[i] = val
524+
allC[val] = true
525+
}
526+
527+
for i := N1; i < N1+N3; i++ {
528+
val := rand.Uint64()
529+
for ok := true; ok; _, ok = allC[val] {
530+
val = rand.Uint64() % 1000
531+
}
532+
otherNums[i] = val
533+
}
534+
535+
sortUint64(commonNums)
536+
sortUint64(blockNums)
537+
sortUint64(otherNums)
538+
539+
return commonNums, blockNums, otherNums
540+
}
541+
496542
func fillNums(N1, N2 int) ([]uint64, []uint64, []uint64) {
497543
rand.Seed(time.Now().UnixNano())
498544

@@ -545,12 +591,12 @@ func TestIntersectCompressedWithLinJump(t *testing.T) {
545591
}
546592

547593
func TestIntersectCompressedWithBin(t *testing.T) {
548-
lengths := []int{0, 1, 3, 11, 100}
594+
//lengths := []int{0, 1, 3, 11, 100, 500, 1000}
549595

550-
for _, N1 := range lengths {
551-
for _, N2 := range lengths {
596+
for _, N1 := range []int{11} {
597+
for _, N2 := range []int{3} {
552598
// Intersection of blockNums and otherNums is commonNums.
553-
commonNums, blockNums, otherNums := fillNums(N1, N2)
599+
commonNums, blockNums, otherNums := fillNumsDiff(N1/10, N1, N2)
554600

555601
enc := codec.Encoder{BlockSize: 10}
556602
for _, num := range blockNums {
@@ -570,7 +616,7 @@ func TestIntersectCompressedWithBin(t *testing.T) {
570616
}
571617

572618
func TestIntersectCompressedWithBinMissingSize(t *testing.T) {
573-
lengths := []int{0, 1, 3, 11, 100}
619+
lengths := []int{0, 1, 3, 11, 100, 500, 1000}
574620

575621
for _, N1 := range lengths {
576622
for _, N2 := range lengths {

codec/codec.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,64 @@ func (d *Decoder) ApproxLen() int {
223223

224224
type searchFunc func(int) bool
225225

226+
// SeekToBlock will find the block containing the uid, and unpack it. When we are going to
227+
// intersect the list later, this function is useful. As this function skips the search function
228+
// and returns the entire block, it is faster than Seek. Unlike seek, we don't truncate the uids
229+
// returned, which would be done by the intersect function anyways.
230+
func (d *Decoder) SeekToBlock(uid uint64, whence seekPos) []uint64 {
231+
if d.Pack == nil {
232+
return []uint64{}
233+
}
234+
prevBlockIdx := d.blockIdx
235+
d.blockIdx = 0
236+
if uid == 0 {
237+
return d.UnpackBlock()
238+
}
239+
240+
// If for some reason we are searching an older uid, we need to search the entire pack
241+
if prevBlockIdx > 0 && uid < d.Pack.Blocks[prevBlockIdx].Base {
242+
prevBlockIdx = 0
243+
}
244+
245+
blocksFunc := func() searchFunc {
246+
var f searchFunc
247+
switch whence {
248+
case SeekStart:
249+
f = func(i int) bool { return d.Pack.Blocks[i+prevBlockIdx].Base >= uid }
250+
case SeekCurrent:
251+
f = func(i int) bool { return d.Pack.Blocks[i+prevBlockIdx].Base > uid }
252+
}
253+
return f
254+
}
255+
256+
idx := sort.Search(len(d.Pack.Blocks[prevBlockIdx:]), blocksFunc()) + prevBlockIdx
257+
// The first block.Base >= uid.
258+
if idx == 0 {
259+
return d.UnpackBlock()
260+
}
261+
// The uid is the first entry in the block.
262+
if idx < len(d.Pack.Blocks) && d.Pack.Blocks[idx].Base == uid {
263+
d.blockIdx = idx
264+
return d.UnpackBlock()
265+
}
266+
267+
// Either the idx = len(pack.Blocks) that means it wasn't found in any of the block's base. Or,
268+
// we found the first block index whose base is greater than uid. In these cases, go to the
269+
// previous block and search there.
270+
d.blockIdx = idx - 1 // Move to the previous block. If blockIdx<0, unpack will deal with it.
271+
if d.blockIdx != prevBlockIdx {
272+
d.UnpackBlock() // And get all their uids.
273+
}
274+
275+
if uid <= d.uids[len(d.uids)-1] {
276+
return d.uids
277+
}
278+
279+
// Could not find any uid in the block, which is >= uid. The next block might still have valid
280+
// entries > uid.
281+
return d.Next()
282+
}
283+
226284
// Seek will search for uid in a packed block using the specified whence position.
227285
// The value of whence must be one of the predefined values SeekStart or SeekCurrent.
228286
// SeekStart searches uid and includes it as part of the results.

0 commit comments

Comments
 (0)