Skip to content

Commit

Permalink
Add SIMD-based separator character lookup
Browse files Browse the repository at this point in the history
Used in decoder.readSimpleName()
  • Loading branch information
kaiburjack committed Sep 21, 2022
1 parent 023fc79 commit 11a90ce
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 7 deletions.
32 changes: 32 additions & 0 deletions combined_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,38 @@ func BenchmarkWithWhitespaceInAttributes(b *testing.B) {
}
}

func BenchmarkLotsOfElementNames(b *testing.B) {
r := strings.NewReader(
`<bookstore>
<bookFromAuthor></bookFromAuthor>
<bookFromAuthor></bookFromAuthor>
<bookFromAuthor></bookFromAuthor>
<bookFromAuthor></bookFromAuthor>
<bookFromAuthor></bookFromAuthor>
<bookFromAuthor></bookFromAuthor>
<bookFromAuthor></bookFromAuthor>
<bookFromAuthor></bookFromAuthor>
<bookFromAuthor></bookFromAuthor>
</bookstore>`)
dec := gosaxml.NewDecoder(r)
var tk gosaxml.Token

b.ReportAllocs()
b.ResetTimer()

for i := 0; i < b.N; i++ {
_, err := r.Seek(0, io.SeekStart)
assert.Nil(b, err)
dec.Reset(r)
for {
err = dec.NextToken(&tk)
if err == io.EOF {
break
}
}
}
}

func decodeEncode(t *testing.T, dec gosaxml.Decoder, enc *gosaxml.Encoder, tk *gosaxml.Token) {
for {
err := dec.NextToken(tk)
Expand Down
2 changes: 1 addition & 1 deletion decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ func isSeparator(b byte) bool {
return int(b) < len(seps) && seps[b]
}

func (thiz *decoder) readSimpleName() ([]byte, byte, error) {
func (thiz *decoder) readSimpleNameGeneric() ([]byte, byte, error) {
i := len(thiz.bb)
for {
j := thiz.r
Expand Down
33 changes: 33 additions & 0 deletions decoder_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,36 @@ func (thiz *decoder) decodeTextAVX2(t *Token) (bool, error) {
}
}
}

func (thiz *decoder) readSimpleName() ([]byte, byte, error) {
if canUseAVX2 {
return thiz.readSimpleNameAVX()
}
return thiz.readSimpleNameGeneric()
}

func (thiz *decoder) readSimpleNameAVX() ([]byte, byte, error) {
i := len(thiz.bb)
for {
j := thiz.r
c := 0
for thiz.w > thiz.r+c {
sidx := int(seperator32(thiz.rb[j+c : thiz.w]))
c += sidx
if sidx != 32 {
_, err := thiz.discard(c + 1)
if err != nil {
return nil, 0, err
}
thiz.bb = append(thiz.bb, thiz.rb[j:j+c]...)
return thiz.bb[i:len(thiz.bb)], thiz.rb[j+c], nil
}
}
thiz.bb = append(thiz.bb, thiz.rb[j:thiz.w]...)
thiz.discardBuffer()
err := thiz.read0()
if err != nil {
return nil, 0, err
}
}
}
4 changes: 4 additions & 0 deletions decoder_arm64.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ func (thiz *decoder) skipWhitespaces(b byte) (byte, error) {
func (thiz *decoder) decodeText(t *Token) (bool, error) {
return thiz.decodeTextGeneric(t)
}

func (thiz *decoder) readSimpleName() ([]byte, byte, error) {
return thiz.readSimpleNameGeneric()
}
3 changes: 3 additions & 0 deletions sse_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ func openAngleBracket32([]uint8) byte

//go:noescape
func onlySpaces32([]uint8) byte

//go:noescape
func seperator32([]uint8) byte
48 changes: 48 additions & 0 deletions sse_amd64.s
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ DATA ·spc<>+16(SB)/8, $0x2020202020202020
DATA ·spc<>+24(SB)/8, $0x2020202020202020
GLOBL ·spc<>(SB), NOPTR+RODATA, $32

DATA ·fifteen<>+0(SB)/8, $0x0F0F0F0F0F0F0F0F
DATA ·fifteen<>+8(SB)/8, $0x0F0F0F0F0F0F0F0F
DATA ·fifteen<>+16(SB)/8, $0x0F0F0F0F0F0F0F0F
DATA ·fifteen<>+24(SB)/8, $0x0F0F0F0F0F0F0F0F
GLOBL ·fifteen<>(SB), NOPTR+RODATA, $32

DATA ·lo_nibbles<>+0(SB)/8, $0x0000000000000010
DATA ·lo_nibbles<>+8(SB)/8, $0x0804820000412000
DATA ·lo_nibbles<>+16(SB)/8, $0x0000000000000010
DATA ·lo_nibbles<>+24(SB)/8, $0x0804820000412000
GLOBL ·lo_nibbles<>(SB), NOPTR+RODATA, $32

DATA ·hi_nibbles<>+0(SB)/8, $0x00000000071800E0
DATA ·hi_nibbles<>+8(SB)/8, $0x0000000000000000
DATA ·hi_nibbles<>+16(SB)/8, $0x00000000071800E0
DATA ·hi_nibbles<>+24(SB)/8, $0x0000000000000000
GLOBL ·hi_nibbles<>(SB), NOPTR+RODATA, $32

TEXT ·openAngleBracket16(SB),NOSPLIT, $0
MOVQ arg+0(FP), DI
MOVOU (DI), X0
Expand Down Expand Up @@ -56,3 +74,33 @@ TEXT ·onlySpaces32(SB),NOSPLIT, $0
MOVB AX, ret+24(FP)
VZEROUPPER // <- https://i.stack.imgur.com/dGpbi.png
RET

TEXT ·seperator32(SB),NOSPLIT, $0
MOVQ arg+0(FP), DI
VMOVDQU (DI), Y0

// http://0x80.pl/articles/simd-byte-lookup.html#special-case-1-small-sets
VPSRLW $4, Y0, Y1
VMOVDQA ·fifteen<>(SB), Y2
VPAND Y2, Y0, Y0
VMOVDQA ·lo_nibbles<>(SB), Y3
VPSHUFB Y0, Y3, Y0
VPAND Y2, Y1, Y1
VMOVDQA ·hi_nibbles<>(SB), Y2
VPSHUFB Y1, Y2, Y1
VPAND Y0, Y1, Y0

// convert non-zero elements to 0xFF
VPXOR X1, X1, X1 // <- generate all zeroes
VPCMPEQB Y1, Y0, Y0 // <- convert all 0x00 to 0xFF and everything else to 0x00
VPCMPEQD Y1, Y1, Y1 // <- generate all ones
VPXOR Y0, Y1, Y0 // <- flip bits in xmm0

// Extract index of first 1 bit
VPMOVMSKB Y0, AX
TZCNTL AX, AX

// return
MOVB AX, ret+24(FP)
VZEROUPPER // <- https://i.stack.imgur.com/dGpbi.png
RET
50 changes: 44 additions & 6 deletions sse_amd64_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,54 @@ import (
)

func TestOpenAngleBracket16(t *testing.T) {
assert.Equal(t, byte(3), openAngleBracket16([]uint8{0x20, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20}))
assert.Equal(t, byte(16), openAngleBracket16([]uint8{0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20}))
assert.Equal(t, byte(3), openAngleBracket16(at(slice(16, ' '), 3, '<')))
assert.Equal(t, byte(15), openAngleBracket16(at(slice(16, ' '), 15, '<')))
assert.Equal(t, byte(16), openAngleBracket16(slice(16, ' ')))
}

func TestOnlySpaces16(t *testing.T) {
assert.Equal(t, byte(3), onlySpaces16([]uint8{0x20, 0x20, 0x20, 0xC2, 0xA7, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20}))
assert.Equal(t, byte(16), onlySpaces16([]uint8{0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20}))
assert.Equal(t, byte(16), onlySpaces16(slice(16, ' ')))
assert.Equal(t, byte(16), onlySpaces16(slice(16, '\t')))
assert.Equal(t, byte(16), onlySpaces16(slice(16, '\n')))
assert.Equal(t, byte(16), onlySpaces16(slice(16, '\t')))
assert.Equal(t, byte(3), onlySpaces16(at(slice(16, '\n'), 3, ':')))
assert.Equal(t, byte(15), onlySpaces16(at(slice(16, ' '), 15, ':')))
assert.Equal(t, byte(3), onlySpaces16(at(at(slice(16, ' '), 3, 0xC2), 4, 0xA7)))
}

func TestOnlySpaces32(t *testing.T) {
assert.Equal(t, byte(3), onlySpaces32([]uint8{0x20, 0x20, 0x20, 0xC2, 0xA7, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20}))
assert.Equal(t, byte(32), onlySpaces32([]uint8{0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20}))
assert.Equal(t, byte(32), onlySpaces32(slice(32, ' ')))
assert.Equal(t, byte(32), onlySpaces32(slice(32, '\t')))
assert.Equal(t, byte(32), onlySpaces32(slice(32, '\n')))
assert.Equal(t, byte(32), onlySpaces32(slice(32, '\t')))
assert.Equal(t, byte(3), onlySpaces32(at(slice(32, '\n'), 3, ':')))
assert.Equal(t, byte(15), onlySpaces32(at(slice(32, ' '), 15, ':')))
assert.Equal(t, byte(31), onlySpaces32(at(slice(32, ' '), 31, ':')))
assert.Equal(t, byte(3), onlySpaces32(at(at(slice(32, ' '), 3, 0xC2), 4, 0xA7)))
}

func TestSeparator32(t *testing.T) {
assert.Equal(t, byte(32), seperator32(slice(32, 'a')))
assert.Equal(t, byte(15), seperator32(at(slice(32, 'a'), 15, ':')))
assert.Equal(t, byte(31), seperator32(at(slice(32, 'a'), 31, ':')))
assert.Equal(t, byte(5), seperator32(at(slice(32, 'a'), 5, '/')))
assert.Equal(t, byte(5), seperator32(at(slice(32, 'a'), 5, '>')))
assert.Equal(t, byte(5), seperator32(at(slice(32, 'a'), 5, '=')))
assert.Equal(t, byte(5), seperator32(at(slice(32, 'a'), 5, ' ')))
assert.Equal(t, byte(5), seperator32(at(slice(32, 'a'), 5, '\t')))
assert.Equal(t, byte(5), seperator32(at(slice(32, 'a'), 5, '\n')))
assert.Equal(t, byte(5), seperator32(at(slice(32, 'a'), 5, '\r')))
}

func slice(len int, v uint8) []uint8 {
s := make([]uint8, len)
for i := range s {
s[i] = v
}
return s
}

func at(s []uint8, i int, v uint8) []uint8 {
s[i] = v
return s
}

0 comments on commit 11a90ce

Please sign in to comment.