Skip to content

Conversation

@Sahil-4555
Copy link
Contributor

This PR makes the gas price oracle faster.

Right now, Erigon collects sampled transaction tips into a heap and then repeatedly pops elements until it reaches the requested percentile. This works correctly, but it does extra work because many elements are popped and discarded.

In practice, the oracle only needs one value (the percentile), not a fully ordered list.
This PR replaces the repeated heap pops with a k-th element (QuickSelect) algorithm, which directly finds the needed percentile value.

The result is the same, but the work done is much smaller.

Test/Benchmark Cases:

const (
	sliceSizeSmall = 20
	sliceSizeLarge = 3600
	percentile     = 60
	iterations     = 20
)

func generateUint256Slice(n int) []*uint256.Int {
	out := make([]*uint256.Int, n)
	for i := 0; i < n; i++ {
		out[i] = uint256.NewInt(uint64(rand.Int63()))
	}
	return out
}

func copyUint256Slice(src []*uint256.Int) []*uint256.Int {
	dst := make([]*uint256.Int, len(src))
	for i, v := range src {
		dst[i] = new(uint256.Int).Set(v)
	}
	return dst
}

type sortingHeap []*uint256.Int

func (s sortingHeap) Len() int           { return len(s) }
func (s sortingHeap) Less(i, j int) bool { return s[i].Lt(s[j]) }
func (s sortingHeap) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

func (s *sortingHeap) Push(x any) {
	*s = append(*s, x.(*uint256.Int))
}

func (s *sortingHeap) Pop() any {
	old := *s
	n := len(old)
	x := old[n-1]
	old[n-1] = nil
	*s = old[:n-1]
	return x
}

func heapPercentile(values []*uint256.Int, percentile int) *uint256.Int {
	h := sortingHeap(values)
	heap.Init(&h)
	pos := (h.Len() - 1) * percentile / 100
	for i := 0; i < pos; i++ {
		heap.Pop(&h)
	}
	return h[0]
}

func partitionUint256(values []*uint256.Int, left, right int) int {
	pivot := values[right]
	i := left
	for j := left; j < right; j++ {
		if values[j].Lt(pivot) {
			values[i], values[j] = values[j], values[i]
			i++
		}
	}
	values[i], values[right] = values[right], values[i]
	return i
}

func findKthUint256(values []*uint256.Int, k int) *uint256.Int {
	left, right := 0, len(values)-1
	for left < right {
		pivot := left + rand.Intn(right-left+1)
		values[pivot], values[right] = values[right], values[pivot]
		pos := partitionUint256(values, left, right)
		if pos == k {
			return values[k]
		} else if pos < k {
			left = pos + 1
		} else {
			right = pos - 1
		}
	}
	return values[left]
}

func TestKthAlgorithmCorrectness(t *testing.T) {
	for i := 0; i < iterations; i++ {
		original := generateUint256Slice(sliceSizeSmall)

		// Create independent copies
		heapCopy := copyUint256Slice(original)
		kthCopy := copyUint256Slice(original)

		// Heap-based percentile (current Erigon behavior)
		heapResult := heapPercentile(heapCopy, percentile)

		// K-th / QuickSelect percentile (optimized behavior)
		index := (len(kthCopy) - 1) * percentile / 100
		kthResult := findKthUint256(kthCopy, index)

		// Verify results match
		if heapResult.Cmp(kthResult) != 0 {
			t.Fatalf(
				"Iteration %d: percentile mismatch\nheap=%s\nkth =%s",
				i,
				heapResult.String(),
				kthResult.String(),
			)
		}
	}
}


func BenchmarkHeapPercentile_N20(b *testing.B) {
	testData := make([][]*uint256.Int, iterations)
	for i := 0; i < iterations; i++ {
		testData[i] = generateUint256Slice(sliceSizeSmall)
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		for j := 0; j < iterations; j++ {
			values := copyUint256Slice(testData[j])
			_ = heapPercentile(values, percentile)
		}
	}
}

func BenchmarkKthPercentile_N20(b *testing.B) {
	testData := make([][]*uint256.Int, iterations)
	for i := 0; i < iterations; i++ {
		testData[i] = generateUint256Slice(sliceSizeSmall)
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		for j := 0; j < iterations; j++ {
			values := copyUint256Slice(testData[j])
			index := (len(values) - 1) * percentile / 100
			_ = findKthUint256(values, index)
		}
	}
}

func BenchmarkHeapPercentile(b *testing.B) {
	testData := make([][]*uint256.Int, b.N)
	for i := 0; i < b.N; i++ {
		testData[i] = generateUint256Slice(sliceSizeLarge)
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		values := copyUint256Slice(testData[i])
		_ = heapPercentile(values, percentile)
	}
}

func BenchmarkKthPercentile(b *testing.B) {
	testData := make([][]*uint256.Int, b.N)
	for i := 0; i < b.N; i++ {
		testData[i] = generateUint256Slice(sliceSizeLarge)
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		values := copyUint256Slice(testData[i])
		index := (len(values) - 1) * percentile / 100
		_ = findKthUint256(values, index)
	}
}

Benchmarks measure only the percentile calculation logic, excluding block loading, RPC calls, and other unrelated work.

Small input (20 values)

goos: linux
goarch: amd64
pkg: github.com/erigontech/erigon/rpc/gasprice
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
BenchmarkHeapPercentile_N20-8              46737             24143 ns/op           16480 B/op        440 allocs/op
BenchmarkKthPercentile_N20-8               61142             19329 ns/op           16000 B/op        420 allocs/op

(24,143 - 19,329) / 24,143 ≈ 20%

Realistic input (~3600 values)
This matches real usage (about 20 blocks with ~180 sampled transactions per block).

goos: linux
goarch: amd64
pkg: github.com/erigontech/erigon/rpc/gasprice
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
BenchmarkHeapPercentile-8                   1959            554593 ns/op          147992 B/op       3602 allocs/op
BenchmarkKthPercentile-8                    7620            197786 ns/op          147968 B/op       3601 allocs/op

(554,593 - 197,786) / 554,593 ≈ 64%

@Sahil-4555
Copy link
Contributor Author

@chfast would love to get your feedback; when you get a chance. Thanks!

@yperbasis
Copy link
Member

@canepat could you review this please?

@Sahil-4555
Copy link
Contributor Author

@yperbasis @canepat ; any chance of this getting added? :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants