lib/protocol: Optimize luhn and chunk functions

These functions were very naive and slow. We haven't done much about
them because they pretty much don't matter at all for Syncthing
performance. They are however called very often in the discovery server
and these optimizations have a huge effect on the CPU load on the
public discovery servers.

The code isn't exactly obvious, but we have good test coverage on all
these functions.

benchmark                 old ns/op     new ns/op     delta
BenchmarkLuhnify-8        12458         1045          -91.61%
BenchmarkUnluhnify-8      12598         1074          -91.47%
BenchmarkChunkify-8       10792         104           -99.04%

benchmark                 old allocs     new allocs     delta
BenchmarkLuhnify-8        18             1              -94.44%
BenchmarkUnluhnify-8      18             1              -94.44%
BenchmarkChunkify-8       44             2              -95.45%

benchmark                 old bytes     new bytes     delta
BenchmarkLuhnify-8        1278          64            -94.99%
BenchmarkUnluhnify-8      1278          64            -94.99%
BenchmarkChunkify-8       42552         128           -99.70%

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4346
This commit is contained in:
Jakob Borg 2017-09-03 10:26:12 +00:00 committed by Audrius Butkevicius
parent 9e6a1fdcd4
commit 9682bbfbda
4 changed files with 66 additions and 25 deletions

View File

@ -8,7 +8,6 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"regexp"
"strings" "strings"
"github.com/calmh/luhn" "github.com/calmh/luhn"
@ -155,16 +154,17 @@ func luhnify(s string) (string, error) {
panic("unsupported string length") panic("unsupported string length")
} }
res := make([]string, 0, 4) res := make([]byte, 4*(13+1))
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
p := s[i*13 : (i+1)*13] p := s[i*13 : (i+1)*13]
copy(res[i*(13+1):], p)
l, err := luhn.Base32.Generate(p) l, err := luhn.Base32.Generate(p)
if err != nil { if err != nil {
return "", err return "", err
} }
res = append(res, fmt.Sprintf("%s%c", p, l)) res[(i+1)*(13)+i] = byte(l)
} }
return res[0] + res[1] + res[2] + res[3], nil return string(res), nil
} }
func unluhnify(s string) (string, error) { func unluhnify(s string) (string, error) {
@ -172,25 +172,31 @@ func unluhnify(s string) (string, error) {
return "", fmt.Errorf("%q: unsupported string length %d", s, len(s)) return "", fmt.Errorf("%q: unsupported string length %d", s, len(s))
} }
res := make([]string, 0, 4) res := make([]byte, 52)
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
p := s[i*14 : (i+1)*14-1] p := s[i*(13+1) : (i+1)*(13+1)-1]
copy(res[i*13:], p)
l, err := luhn.Base32.Generate(p) l, err := luhn.Base32.Generate(p)
if err != nil { if err != nil {
return "", err return "", err
} }
if g := fmt.Sprintf("%s%c", p, l); g != s[i*14:(i+1)*14] { if s[(i+1)*14-1] != byte(l) {
return "", fmt.Errorf("%q: check digit incorrect", s) return "", fmt.Errorf("%q: check digit incorrect", s)
} }
res = append(res, p)
} }
return res[0] + res[1] + res[2] + res[3], nil return string(res), nil
} }
func chunkify(s string) string { func chunkify(s string) string {
s = regexp.MustCompile("(.{7})").ReplaceAllString(s, "$1-") chunks := len(s) / 7
s = strings.Trim(s, "-") res := make([]byte, chunks*(7+1)-1)
return s for i := 0; i < chunks; i++ {
if i > 0 {
res[i*(7+1)-1] = '-'
}
copy(res[i*(7+1):], s[i*7:(i+1)*7])
}
return string(res)
} }
func unchunkify(s string) string { func unchunkify(s string) string {

View File

@ -150,3 +150,41 @@ func TestNewDeviceIDMarshalling(t *testing.T) {
t.Error("Mismatch in old -> new direction") t.Error("Mismatch in old -> new direction")
} }
} }
var resStr string
func BenchmarkLuhnify(b *testing.B) {
str := "ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJAB"
var err error
for i := 0; i < b.N; i++ {
resStr, err = luhnify(str)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkUnluhnify(b *testing.B) {
str, _ := luhnify("ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJAB")
var err error
for i := 0; i < b.N; i++ {
resStr, err = unluhnify(str)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkChunkify(b *testing.B) {
str := "ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJAB"
for i := 0; i < b.N; i++ {
resStr = chunkify(str)
}
}
func BenchmarkUnchunkify(b *testing.B) {
str := chunkify("ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJAB")
for i := 0; i < b.N; i++ {
resStr = unchunkify(str)
}
}

21
vendor/github.com/calmh/luhn/luhn.go generated vendored
View File

@ -19,10 +19,6 @@ var (
// Generate returns a check digit for the string s, which should be composed // Generate returns a check digit for the string s, which should be composed
// of characters from the Alphabet a. // of characters from the Alphabet a.
func (a Alphabet) Generate(s string) (rune, error) { func (a Alphabet) Generate(s string) (rune, error) {
if err := a.check(); err != nil {
return 0, err
}
factor := 1 factor := 1
sum := 0 sum := 0
n := len(a) n := len(a)
@ -57,14 +53,15 @@ func (a Alphabet) Validate(s string) bool {
return rune(s[len(s)-1]) == c return rune(s[len(s)-1]) == c
} }
// check returns an error if the given alphabet does not consist of unique characters // NewAlphabet converts the given string an an Alphabet, verifying that it
func (a Alphabet) check() error { // is correct.
cm := make(map[byte]bool, len(a)) func NewAlphabet(s string) (Alphabet, error) {
for i := range a { cm := make(map[byte]bool, len(s))
if cm[a[i]] { for i := range s {
return fmt.Errorf("Digit %q non-unique in alphabet %q", a[i], a) if cm[s[i]] {
return "", fmt.Errorf("Digit %q non-unique in alphabet %q", s[i], s)
} }
cm[a[i]] = true cm[s[i]] = true
} }
return nil return Alphabet(s), nil
} }

2
vendor/manifest vendored
View File

@ -53,7 +53,7 @@
"importpath": "github.com/calmh/luhn", "importpath": "github.com/calmh/luhn",
"repository": "https://github.com/calmh/luhn", "repository": "https://github.com/calmh/luhn",
"vcs": "git", "vcs": "git",
"revision": "0c8388ff95fa92d4094011e5a04fc99dea3d1632", "revision": "c0f1d77264fb3d1bfc65b70eea6ee264058c57c0",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },