fork of https://github.com/sourcegraph/zoekt
0

Configure Feed

Select the types of activity you want to include in your feed.

read: inline delta-varint decoding in unmarshalDocumentSections (#375)

In the profiles of zoekt-webserver, this function accounts for as much
as 30% of CPU and 87% of allocated memory. While it seems a promising
direction to investigate better storage and/or reading strategies here,
removing unnecessary allocation and copying is an easy win for now.
Besides, there was an old TODO where I think there author had something
similar to this commit in mind.

The distribution in the benchmark is synthetic, and the gap sizes come
from (non-rigorous) averaging a sample from the indexed source code I happened
to have in my local zoekt copy. However, if they are anywhere close
to real, per the benchstat result below I expect to see a 1.0 - (0.3 * 0.75 + 0.7) = 7.5%
overall speed boost, and even better results for memory allocations.

name old time/op new time/op delta
UnmarshalDocSections/10-10 80.1ns ± 1% 52.5ns ± 1% -34.53% (p=0.000 n=10+9)
UnmarshalDocSections/100-10 602ns ± 1% 421ns ± 1% -30.17% (p=0.000 n=10+9)
UnmarshalDocSections/1000-10 7.30µs ± 1% 5.30µs ± 2% -27.41% (p=0.000 n=10+10)
UnmarshalDocSections/10000-10 72.2µs ± 2% 59.9µs ± 1% -17.02% (p=0.000 n=10+9)

name old alloc/op new alloc/op delta
UnmarshalDocSections/10-10 160B ± 0% 80B ± 0% -50.00% (p=0.000 n=10+10)
UnmarshalDocSections/100-10 1.79kB ± 0% 0.90kB ± 0% -50.00% (p=0.000 n=10+10)
UnmarshalDocSections/1000-10 16.4kB ± 0% 8.2kB ± 0% -50.00% (p=0.000 n=10+10)
UnmarshalDocSections/10000-10 164kB ± 0% 82kB ± 0% -50.00% (p=0.000 n=10+10)

name old allocs/op new allocs/op delta
UnmarshalDocSections/10-10 2.00 ± 0% 1.00 ± 0% -50.00% (p=0.000 n=10+10)
UnmarshalDocSections/100-10 2.00 ± 0% 1.00 ± 0% -50.00% (p=0.000 n=10+10)
UnmarshalDocSections/1000-10 2.00 ± 0% 1.00 ± 0% -50.00% (p=0.000 n=10+10)
UnmarshalDocSections/10000-10 2.00 ± 0% 1.00 ± 0% -50.00% (p=0.000 n=10+10)

+83 -13
+1 -1
api.go
··· 320 320 } 321 321 322 322 func (r *Repository) UnmarshalJSON(data []byte) error { 323 - // We define a new type so that we can use json.Unmarhsal 323 + // We define a new type so that we can use json.Unmarshal 324 324 // without recursing into this same method. 325 325 type repository *Repository 326 326 repo := repository(r)
+27 -12
bits.go
··· 184 184 return toSizedDeltas(ints) 185 185 } 186 186 187 - func unmarshalDocSections(in []byte, buf []DocumentSection) (secs []DocumentSection) { 187 + func unmarshalDocSections(data []byte, ds []DocumentSection) []DocumentSection { 188 188 // Defensive, this shouldn't happen. While we have the feature flag for lazy 189 - // doc section decoding lets be extra defensive. 190 - if len(in) == 0 { 189 + // doc section decoding let's be extra defensive. 190 + if len(data) == 0 { 191 191 log.Println("WARN unmarshalDocSections received an empty slice to unmarshal") 192 192 return nil 193 193 } 194 194 195 - // TODO - ints is unnecessary garbage here. 196 - ints := fromSizedDeltas(in, nil) 197 - if cap(buf) >= len(ints)/2 { 198 - buf = buf[:0] 195 + sz, m := binary.Uvarint(data) 196 + data = data[m:] 197 + 198 + if cap(ds) < int(sz)/2 { 199 + ds = make([]DocumentSection, 0, sz/2) 199 200 } else { 200 - buf = make([]DocumentSection, 0, len(ints)/2) 201 + ds = ds[:0] 201 202 } 202 203 203 - for len(ints) > 0 { 204 - buf = append(buf, DocumentSection{ints[0], ints[1]}) 205 - ints = ints[2:] 204 + // Inlining the delta decoding to avoid unnecessary allocations that would come 205 + // from the straightforward implementation, i.e. packing the result of fromSizedDeltas. 206 + var last uint32 207 + for len(data) > 0 { 208 + var d DocumentSection 209 + 210 + delta, m := binary.Uvarint(data) 211 + last += uint32(delta) 212 + data = data[m:] 213 + d.Start = last 214 + 215 + delta, m = binary.Uvarint(data) 216 + last += uint32(delta) 217 + data = data[m:] 218 + d.End = last 219 + 220 + ds = append(ds, d) 206 221 } 207 - return buf 222 + return ds 208 223 } 209 224 210 225 type ngramSlice []ngram
+55
bits_test.go
··· 20 20 "math/rand" 21 21 "reflect" 22 22 "sort" 23 + "strconv" 23 24 "testing" 24 25 "testing/quick" 25 26 ··· 260 261 } 261 262 } 262 263 } 264 + 265 + func TestUnmarshalDocSections(t *testing.T) { 266 + f := func(nums []uint32) bool { 267 + nums = sortedUnique(nums) 268 + 269 + ds := make([]DocumentSection, 0, len(nums)/2) 270 + // Drop the unmatched last one in case of odd len(nums). 271 + // Better than skipping the test entirely. 272 + for i := 0; i+1 < len(nums); i += 2 { 273 + ds = append(ds, DocumentSection{Start: nums[i], End: nums[i+1]}) 274 + } 275 + 276 + data := marshalDocSections(ds) 277 + got := unmarshalDocSections(data, nil) 278 + if len(ds) == len(got) && len(ds) == 0 { 279 + return true 280 + } 281 + if !reflect.DeepEqual(ds, got) { 282 + t.Log(cmp.Diff(ds, got)) 283 + return false 284 + } 285 + return true 286 + } 287 + if err := quick.Check(f, nil); err != nil { 288 + t.Error(err) 289 + } 290 + } 291 + 292 + func BenchmarkUnmarshalDocSections(b *testing.B) { 293 + rng := rand.New(rand.NewSource(0)) 294 + 295 + for size := 10; size <= 10000; size *= 10 { 296 + ds := make([]DocumentSection, 0, size) 297 + var last uint32 298 + for i := 0; i < size; i++ { 299 + var d DocumentSection 300 + last += 1 + uint32(rng.Int31n(200)) 301 + d.Start = last 302 + last += 1 + uint32(rng.Int31n(10)) 303 + d.End = last 304 + ds = append(ds, d) 305 + } 306 + data := marshalDocSections(ds) 307 + 308 + b.Run(strconv.Itoa(size), func(b *testing.B) { 309 + b.ReportAllocs() 310 + for i := 0; i < b.N; i++ { 311 + if ds := unmarshalDocSections(data, nil); len(ds) != size { 312 + b.Fatalf("wrong size: got %d, want %d", len(ds), size) 313 + } 314 + } 315 + }) 316 + } 317 + }