Skip to content

Commit 9ccb276

Browse files
committed
improve defaultSize adjustment for rare case when next few j > 0 p.calls after sort (==> a) have a[j].calls ~= a[0].calls and some of them a[j].size > defaultSize (== a[0].size) + added test TestPoolCalibrateWithAdjustment
+ also replaced floating-point arithmetic with integer muldiv equivalent + fix tests fn allocNBytes() + some microoptimizations
1 parent 18533fa commit 9ccb276

File tree

2 files changed

+145
-6
lines changed

2 files changed

+145
-6
lines changed

pool.go

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@ const (
1515

1616
calibrateCallsThreshold = 42000
1717
maxPercentile = 0.95
18+
19+
callsSumMaxValue = steps * calibrateCallsThreshold
20+
21+
fractionDenominator = uint64(100) // denominator of regular fractions
22+
23+
// regular fraction of maxPercentile
24+
maxPercentileRNumer = uint64(maxPercentile * float64(fractionDenominator)) // numerator of maxPercentile
25+
maxPercentileGcd = uint64(5) // gcd(maxPercentileRNumer, fractionDenominator) = gcd(int(maxPercentile * 100), 100)
26+
maxPercentileNumer = maxPercentileRNumer / maxPercentileGcd // simplified numerator of maxPercentile
27+
maxPercentileDenom = fractionDenominator / maxPercentileGcd // simplified denominator of maxPercentile
28+
29+
// allowable size spread for DefaultSize additional adjustment
30+
calibrateDefaultSizeAdjustmentsSpread = 0.05 // down to 5% of initial DefaultSize` calls count
31+
calibrateDefaultSizeAdjustmentsFactor = 1 - calibrateDefaultSizeAdjustmentsSpread // see calibrate() below
32+
33+
// regular fraction of calibrateDefaultSizeAdjustmentsFactor
34+
calibrateDefaultSizeAdjustmentsFactorRNumer = uint64(calibrateDefaultSizeAdjustmentsFactor * float64(fractionDenominator)) // numerator of calibrateDefaultSizeAdjustmentsFactor
35+
calibrateDSASGcd = uint64(5) // gcd(calibrateDefaultSizeAdjustmentsFactorRNumer, fractionDenominator)
36+
calibrateDefaultSizeAdjustmentsFactorNumer = calibrateDefaultSizeAdjustmentsFactorRNumer / calibrateDSASGcd // simplified numerator of calibrateDefaultSizeAdjustmentsFactor
37+
calibrateDefaultSizeAdjustmentsFactorDenom = fractionDenominator / calibrateDSASGcd // simplified denominator of calibrateDefaultSizeAdjustmentsFactor
1838
)
1939

2040
// Pool represents byte buffer pool.
@@ -84,7 +104,9 @@ func (p *Pool) calibrate() {
84104
}
85105

86106
a := make(callSizes, 0, steps)
87-
var callsSum uint64
107+
108+
callsSum := uint64(0)
109+
88110
for i := uint64(0); i < steps; i++ {
89111
calls := atomic.SwapUint64(&p.calls[i], 0)
90112
callsSum += calls
@@ -98,17 +120,43 @@ func (p *Pool) calibrate() {
98120
defaultSize := a[0].size
99121
maxSize := defaultSize
100122

101-
maxSum := uint64(float64(callsSum) * maxPercentile)
102-
callsSum = 0
103-
for i := 0; i < steps; i++ {
123+
// callsSum <= steps * calibrateCallsThreshold + maybe small R = callsSumMaxValue + R <<<< (MaxUint64 / fractionDenominator),
124+
// maxPercentileNumer < fractionDenominator, therefore, integer multiplication by a fraction can be used without overflow
125+
maxSum := (callsSum * maxPercentileNumer) / maxPercentileDenom // == uint64(callsSum * maxPercentile)
126+
127+
// avoid visiting a[0] one more times in `for` loop below
128+
callsSum = a[0].calls
129+
130+
// defaultSize adjust cond:
131+
// ( abs(a[0].calls - a[i].calls) < a[0].calls * calibrateDefaultSizeAdjustmentsSpread ) && ( defaultSize < a[i].size )
132+
// due to fact that a is sorted by calls desc,
133+
// abs(a[0].calls - a[i].calls) === a[0].calls - a[i].calls ==>
134+
// a[0].calls - a[i].calls < a[0].calls * calibrateDefaultSizeAdjustmentsSpread ==>
135+
// a[0].calls - a[0].calls * calibrateDefaultSizeAdjustmentsSpread < a[i].calls ==>
136+
// a[i].calls > a[0].calls * (1 - calibrateDefaultSizeAdjustmentsSpread) ==>
137+
// a[i].calls > a[0].calls * calibrateDefaultSizeAdjustmentsFactor
138+
// and we can pre-calculate a[0].calls * calibrateDefaultSizeAdjustmentsFactor
139+
140+
// a[0].calls ~= calibrateCallsThreshold + maybe small R <<<< (MaxUint64 / fractionDenominator)
141+
defSizeAdjustCallsThreshold := (a[0].calls * calibrateDefaultSizeAdjustmentsFactorNumer) / calibrateDefaultSizeAdjustmentsFactorDenom // == uint64(a[0].calls * calibrateDefaultSizeAdjustmentsFactor)
142+
143+
for i := 1; i < steps; i++ {
144+
104145
if callsSum > maxSum {
105146
break
106147
}
107-
callsSum += a[i].calls
148+
108149
size := a[i].size
150+
151+
if (a[i].calls > defSizeAdjustCallsThreshold) && (size > defaultSize) {
152+
defaultSize = size
153+
}
154+
109155
if size > maxSize {
110156
maxSize = size
111157
}
158+
159+
callsSum += a[i].calls
112160
}
113161

114162
atomic.StoreUint64(&p.defaultSize, defaultSize)

pool_test.go

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package bytebufferpool
22

33
import (
4+
"math/bits"
45
"math/rand"
56
"testing"
67
"time"
@@ -40,6 +41,43 @@ func TestPoolCalibrate(t *testing.T) {
4041
}
4142
}
4243

44+
func TestPoolCalibrateWithAdjustment(t *testing.T) {
45+
46+
var p Pool
47+
48+
const n = 510
49+
50+
adjN := n << 2
51+
52+
// smaller buffer
53+
allocNBytesMtimes(&p, n, calibrateCallsThreshold-10)
54+
55+
// t.Log(p.calls)
56+
57+
// never trigger calibrate, never used as adjustment for defaultSize
58+
for i, s := 0, adjN<<4; i < calibrateCallsThreshold>>1; i++ {
59+
v := s + rand.Intn(maxSize)
60+
allocNBytesInP(&p, v)
61+
}
62+
63+
// larger buffer
64+
allocNBytesMtimes(&p, adjN, calibrateCallsThreshold-10)
65+
66+
// t.Log(p.calls)
67+
68+
// now throw away existing larger buf from pool
69+
_ = p.Get()
70+
71+
// ... and now finish with new smaller buf (emulate a long process that uses it)
72+
allocNBytesMtimes(&p, n, 11)
73+
74+
// t.Logf("%#v", p)
75+
76+
if v := powOfTwo64(uint64(adjN)); v != p.defaultSize {
77+
t.Fatalf("wrong pool final defaultSize: want %d, got %d", v, p.defaultSize)
78+
}
79+
}
80+
4381
func TestPoolVariousSizesSerial(t *testing.T) {
4482
testPoolVariousSizes(t)
4583
}
@@ -62,6 +100,18 @@ func TestPoolVariousSizesConcurrent(t *testing.T) {
62100
}
63101
}
64102

103+
//go:noinline
104+
func TestIntArithmetic(t *testing.T) {
105+
106+
if float64(maxPercentileNumer) != (float64(maxPercentile) * float64(maxPercentileDenom)) {
107+
t.Fatalf("wrong maxPercentile interpolation: want %f, got %f", maxPercentile, float64(maxPercentileNumer)/float64(maxPercentileDenom))
108+
}
109+
110+
if float64(calibrateDefaultSizeAdjustmentsFactorNumer) != (float64(calibrateDefaultSizeAdjustmentsFactor) * float64(calibrateDefaultSizeAdjustmentsFactorDenom)) {
111+
t.Fatalf("wrong maxPercentile interpolation: want %f, got %f", calibrateDefaultSizeAdjustmentsFactor, float64(calibrateDefaultSizeAdjustmentsFactorNumer)/float64(calibrateDefaultSizeAdjustmentsFactorDenom))
112+
}
113+
}
114+
65115
func testPoolVariousSizes(t *testing.T) {
66116
for i := 0; i < steps+1; i++ {
67117
n := (1 << uint32(i))
@@ -90,5 +140,46 @@ func allocNBytes(dst []byte, n int) []byte {
90140
if diff <= 0 {
91141
return dst[:n]
92142
}
93-
return append(dst, make([]byte, diff)...)
143+
// must return buffer with len == requested size n, not `n - cap(dst)`
144+
return append(dst[:cap(dst)], make([]byte, diff)...)
145+
}
146+
147+
func allocNBytesInP(p *Pool, n int) {
148+
b := p.Get()
149+
b.B = allocNBytes(b.B, n)
150+
p.Put(b)
151+
}
152+
153+
func allocNBytesMtimes(p *Pool, n, limit int) {
154+
for i := 0; i < limit; i++ {
155+
allocNBytesInP(p, n)
156+
}
157+
}
158+
159+
// 2^z >= n with min(z)
160+
func powOfTwo64(n uint64) uint64 {
161+
// ((n - 1) & n) - remove the leftmost one bit, 2^k ==> 0, 0 ==> 0, others > 0
162+
// ((n - 1) & n) >> 1 - place for sign to avoid overflow, 2^k ==> 0, 0 ==> 0, others > 0
163+
// ^(((n - 1) & n) >> 1) - invert result, 2^k ==> uint64(-1), 0 ==> uint64(-1), others < -1
164+
// (^(((n - 1) & n) >> 1) + 1) - for 2^k ==> 0, 0 ==> 0, others < 0
165+
// uint(^(((n - 1) & n) >> 1) + 1 - z) >> 63 - got sign of result as leftmost bit, 2^k -> 0, 0 -> 0, others -> 1
166+
a := int(uint64(^(((n-1)&n)>>1)+1) >> 63)
167+
z := int(((n - 1) &^ n) >> 63) // 0 -> 1, others -> 0
168+
return 1 << uint(bits.Len64(n)-1+z+a)
169+
}
170+
171+
func allocNMBytesInP(p *Pool, n, m int) {
172+
// ATN! preserve order, its important
173+
bn := p.Get()
174+
bm := p.Get()
175+
bn.B = allocNBytes(bn.B, n)
176+
bm.B = allocNBytes(bm.B, m)
177+
p.Put(bn)
178+
p.Put(bm)
179+
}
180+
181+
func allocNMBytesXtimes(p *Pool, n, m int, limit int) {
182+
for i := 0; i < limit; i++ {
183+
allocNMBytesInP(p, n, m)
184+
}
94185
}

0 commit comments

Comments
 (0)