Performance tuning for plugin/file (#7658)

* plugin/file: improve performance of function tree.less(..)

PrevLabel always begins its iteration from the tail of domain name.
less(..) loop can improve its performance by calling PrevLabel starting
from the last processed label.

As the benchmark results showed, the performance is improved by about 15%.
$ go test -bench=Less -run=^$
goos: linux
goarch: amd64
pkg: github.com/coredns/coredns/plugin/file/tree
cpu: Intel(R) Xeon(R) Platinum 8336C CPU @ 2.30GHz
BenchmarkLess/base-16              99003             12105 ns/op
BenchmarkLess/optimized-16        114522             10590 ns/op
PASS
ok      github.com/coredns/coredns/plugin/file/tree     2.416s

Signed-off-by: yuwenchao <ywc689@163.com>

* plugin/file: performance enhancement for nameFromRight(..)

Similar to tree.less(..), performance of function nameFromRight
can boost by utilizing dns.PrevLabel more efficiently.

As benchmark tests shown, performance of this function with the
optimization is gained by double or triple.

* Benchmark test result for the original implementation:
BenchmarkNameFromRight/i0_origin-16     430719652                2.794 ns/op
BenchmarkNameFromRight/eq_origin_i1_shot-16             30933135                37.52 ns/op
BenchmarkNameFromRight/two_labels_i1-16                 29375857                40.71 ns/op
BenchmarkNameFromRight/two_labels_i2-16                 18556830                63.97 ns/op
BenchmarkNameFromRight/two_labels_i3_shot-16            14678812                84.73 ns/op
BenchmarkNameFromRight/ten_labels_i5-16                  8522132               133.0 ns/op
BenchmarkNameFromRight/ten_labels_i11_shot-16            3154410               378.2 ns/op
BenchmarkNameFromRight/not_subdomain_shot-16            35297224                33.59 ns/op
BenchmarkNameFromRightRandomized-16                     10638702               113.4 ns/op             0 B/op          0 allocs/op

* Benchmark test result with this optimization:
BenchmarkNameFromRight/i0_origin-16     425864671                2.808 ns/op
BenchmarkNameFromRight/eq_origin_i1_shot-16             60903428                19.53 ns/op
BenchmarkNameFromRight/two_labels_i1-16                 50209297                24.21 ns/op
BenchmarkNameFromRight/two_labels_i2-16                 42483711                27.88 ns/op
BenchmarkNameFromRight/two_labels_i3_shot-16            40898925                29.24 ns/op
BenchmarkNameFromRight/ten_labels_i5-16                 27916532                44.54 ns/op
BenchmarkNameFromRight/ten_labels_i11_shot-16           17540040                67.59 ns/op
BenchmarkNameFromRight/not_subdomain_shot-16            67180514                17.46 ns/op
BenchmarkNameFromRightRandomized-16                     32692081                38.21 ns/op            0 B/op          0 allocs/op

Signed-off-by: yuwenchao <yuwenchao@bytedance.com>

---------

Signed-off-by: yuwenchao <ywc689@163.com>
Signed-off-by: yuwenchao <yuwenchao@bytedance.com>
Co-authored-by: yuwenchao <yuwenchao@bytedance.com>
This commit is contained in:
wencyu
2025-11-07 05:12:49 +08:00
committed by GitHub
parent 59afd4b65e
commit deae7ec345
4 changed files with 166 additions and 9 deletions

View File

@@ -16,12 +16,11 @@ import (
// //
// The values of a and b are *not* lowercased before the comparison! // The values of a and b are *not* lowercased before the comparison!
func less(a, b string) int { func less(a, b string) int {
i := 1
aj := len(a) aj := len(a)
bj := len(b) bj := len(b)
for { for {
ai, oka := dns.PrevLabel(a, i) ai, oka := dns.PrevLabel(a[:aj], 1)
bi, okb := dns.PrevLabel(b, i) bi, okb := dns.PrevLabel(b[:bj], 1)
if oka && okb { if oka && okb {
return 0 return 0
} }
@@ -38,7 +37,6 @@ func less(a, b string) int {
return res return res
} }
i++
aj, bj = ai, bi aj, bj = ai, bi
} }
} }

View File

@@ -1,6 +1,7 @@
package tree package tree
import ( import (
"bytes"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -119,3 +120,70 @@ func TestLess_ConcurrentNameAccess(t *testing.T) {
} }
wg.Wait() wg.Wait()
} }
func BenchmarkLess(b *testing.B) {
// The original less function, serving as the benchmark test baseline.
less0 := func(a, b string) int {
i := 1
aj := len(a)
bj := len(b)
for {
ai, oka := dns.PrevLabel(a, i)
bi, okb := dns.PrevLabel(b, i)
if oka && okb {
return 0
}
// sadly this []byte will allocate... TODO(miek): check if this is needed
// for a name, otherwise compare the strings.
ab := []byte(strings.ToLower(a[ai:aj]))
bb := []byte(strings.ToLower(b[bi:bj]))
doDDD(ab)
doDDD(bb)
res := bytes.Compare(ab, bb)
if res != 0 {
return res
}
i++
aj, bj = ai, bi
}
}
tests := []set{
{"aaa.powerdns.de", "bbb.powerdns.net.", "xxx.powerdns.com."},
{"aaa.POWERDNS.de", "bbb.PoweRdnS.net.", "xxx.powerdns.com."},
{"aaa.aaaa.aa.", "aa.aaa.a.", "bbb.bbbb.bb."},
{"aaaaa.", "aaa.", "bbb."},
{"a.a.a.a.", "a.a.", "a.a.a."},
{"example.", "z.example.", "a.example."},
{"a.example.", "Z.a.example.", "z.example.", "yljkjljk.a.example.", "\\001.z.example.", "example.", "*.z.example.", "\\200.z.example.", "zABC.a.EXAMPLE."},
{"a.example.", "Z.a.example.", "z.example.", "yljkjljk.a.example.", "example.", "*.z.example.", "zABC.a.EXAMPLE."},
}
b.ResetTimer()
b.Run("base", func(b *testing.B) {
for b.Loop() {
for _, t := range tests {
for m := range len(t) - 1 {
for n := m + 1; n < len(t); n++ {
less0(t[m], t[n])
}
}
}
}
})
b.Run("optimized", func(b *testing.B) {
for b.Loop() {
for _, t := range tests {
for m := range len(t) - 1 {
for n := m + 1; n < len(t); n++ {
less(t[m], t[n])
}
}
}
}
})
}

View File

@@ -163,19 +163,22 @@ func (z *Zone) nameFromRight(qname string, i int) (string, bool) {
return z.origin, false return z.origin, false
} }
n := len(qname)
for j := 1; j <= z.origLen; j++ { for j := 1; j <= z.origLen; j++ {
if _, shot := dns.PrevLabel(qname, j); shot { if m, shot := dns.PrevLabel(qname[:n], 1); shot {
return qname, shot return qname, shot
} else {
n = m
} }
} }
k := 0
var shot bool
for j := 1; j <= i; j++ { for j := 1; j <= i; j++ {
k, shot = dns.PrevLabel(qname, j+z.origLen) m, shot := dns.PrevLabel(qname[:n], 1)
if shot { if shot {
return qname, shot return qname, shot
} else {
n = m
} }
} }
return qname[k:], false return qname[n:], false
} }

View File

@@ -1,6 +1,8 @@
package file package file
import ( import (
"strconv"
"strings"
"testing" "testing"
"github.com/coredns/coredns/plugin/file/tree" "github.com/coredns/coredns/plugin/file/tree"
@@ -35,6 +37,92 @@ func TestNameFromRight(t *testing.T) {
} }
} }
// Benchmark: measure performance across representative inputs and overshoot cases.
func BenchmarkNameFromRight(b *testing.B) {
origin := "example.org."
a := &Zone{origin: origin, origLen: dns.CountLabel(origin)}
cases := []struct {
name string
qname string
i int
}{
{"i0_origin", origin, 0},
{"eq_origin_i1_shot", origin, 1},
{"two_labels_i1", "a.b." + origin, 1},
{"two_labels_i2", "a.b." + origin, 2},
{"two_labels_i3_shot", "a.b." + origin, 3},
{"ten_labels_i5", strings.Repeat("a.", 10) + origin, 5},
{"ten_labels_i11_shot", strings.Repeat("a.", 10) + origin, 11},
{"not_subdomain_shot", "other.tld.", 1},
}
var sink int
for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
qn, i := tc.qname, tc.i
for b.Loop() {
s, shot := a.nameFromRight(qn, i)
sink += len(s)
if shot {
sink++
}
}
})
}
if sink == 42 { // prevent elimination
b.Log(sink)
}
}
// BenchmarkNameFromRightRandomized iterates over a prebuilt pool
// of qnames and i values to emulate mixed workloads.
func BenchmarkNameFromRightRandomized(b *testing.B) {
origin := "example.org."
a := &Zone{origin: origin, origLen: dns.CountLabel(origin)}
const poolSize = 1024
type pair struct {
q string
i int
}
pool := make([]pair, 0, poolSize)
// Build a variety of qnames with 1..8 labels before origin and various i, including overshoot.
for n := 1; n <= 8; n++ {
var sb strings.Builder
for k := range n {
sb.WriteString("l")
sb.WriteString(strconv.Itoa(k))
sb.WriteByte('.')
}
sb.WriteString(origin)
q := sb.String()
for i := 0; i <= n+2; i++ {
pool = append(pool, pair{q: q, i: i})
}
}
// Add some non-subdomain and shorter-than-origin cases.
pool = append(pool, pair{"org.", 1}, pair{"other.tld.", 1})
// Ensure pool length is power-of-two for fast masking; if not, trim.
// Here we just rely on modulo since the pool isn't huge.
b.ReportAllocs()
var sink int
for n := range b.N {
p := pool[n%len(pool)]
s, shot := a.nameFromRight(p.q, p.i)
sink += len(s)
if shot {
sink++
}
}
if sink == 43 {
b.Log(sink)
}
}
func TestInsertPreservesSRVCase(t *testing.T) { func TestInsertPreservesSRVCase(t *testing.T) {
z := NewZone("home.arpa.", "stdin") z := NewZone("home.arpa.", "stdin")