2
0
Fork 0
mirror of https://github.com/ii64/sonic.git synced 2026-06-21 00:46:43 +08:00

opt: use simd to optimize htmlescape (#171)

* opt: use simd to optimize htmlescape

* opt: reuse escaped buffer

* feat: cmake with Clang12

Co-authored-by: liuqiang <liuqiang.06@bytedance.com>
Co-authored-by: duanyi.aster <duanyi.aster@bytedance.com>
This commit is contained in:
liu 2022-01-18 11:30:28 +08:00 committed by GitHub
parent 14121d64f1
commit d75ce3f730
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 5203 additions and 3317 deletions

View file

@ -21,7 +21,9 @@ import (
`encoding/json`
`reflect`
`runtime`
`unsafe`
`github.com/bytedance/sonic/internal/native`
`github.com/bytedance/sonic/internal/rt`
`github.com/bytedance/sonic/option`
)
@ -102,7 +104,6 @@ func Encode(val interface{}, opts Options) ([]byte, error) {
return nil, err
}
/* EscapeHTML has already returned a new buffer*/
if opts & EscapeHTML != 0 {
return buf, nil
}
@ -131,10 +132,9 @@ func EncodeInto(buf *[]byte, val interface{}, opts Options) error {
/* EscapeHTML needs to allocate a new buffer*/
if opts & EscapeHTML != 0 {
dst := bytes.NewBuffer(make([]byte, 0, len(*buf)))
json.HTMLEscape(dst, *buf)
freeBytes(*buf)
*buf = dst.Bytes()
dest := HTMLEscape(nil, *buf)
freeBytes(*buf) // free origin used buffer
*buf = dest
}
/* avoid GC ahead */
@ -143,6 +143,48 @@ func EncodeInto(buf *[]byte, val interface{}, opts Options) error {
return err
}
var typeByte = rt.UnpackType(reflect.TypeOf(byte(0)))
// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029
// characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029
// so that the JSON will be safe to embed inside HTML <script> tags.
// For historical reasons, web browsers don't honor standard HTML
// escaping within <script> tags, so an alternative JSON encoding must
// be used.
func HTMLEscape(dest []byte, src []byte) []byte {
nb := len(src)
// initilize dest buffer
cap := nb * 6 / 5
if dest == nil {
dest = make([]byte, 0, cap)
}
ds := (*rt.GoSlice)(unsafe.Pointer(&dest))
sp := (*rt.GoSlice)(unsafe.Pointer(&src)).Ptr
ds.Len = 0
if (ds.Cap < cap) {
*ds = growslice(typeByte, *ds, cap)
}
for nb > 0 {
dp := unsafe.Pointer(uintptr(ds.Ptr) + uintptr(ds.Len))
dn := ds.Cap - ds.Len
ret := native.HTMLEscape(sp, nb, dp, &dn)
ds.Len += dn
if ret >= 0 {
break
}
ret = ^ret
nb -= ret
*ds = growslice(typeByte, *ds, ds.Cap * 2)
sp = unsafe.Pointer(uintptr(sp) + uintptr(ret))
}
return dest
}
// EncodeIndented is like Encode but applies Indent to format the output.
// Each JSON element in the output will begin on a new line beginning with prefix
// followed by one or more copies of indent according to the indentation nesting.

View file

@ -17,6 +17,7 @@
package encoder
import (
`bytes`
`encoding/json`
`runtime`
`runtime/debug`
@ -190,6 +191,14 @@ func TestEncoder_EscapeHTML(t *testing.T) {
require.Equal(t, `{"&&":{"X":"<>"}}`, string(ret))
}
func TestEncoder_EscapeHTML_LargeJson(t *testing.T) {
buf1, err1 := Encode(&_BindingValue, SortMapKeys | EscapeHTML)
require.NoError(t, err1)
buf2, err2 :=json.Marshal(&_BindingValue)
require.NoError(t, err2)
require.Equal(t, buf1, buf2)
}
var _GenericValue interface{}
var _BindingValue TwitterStruct
@ -287,6 +296,17 @@ func BenchmarkEncoder_Binding_SonicSorted(b *testing.B) {
}
}
func BenchmarkEncoder_Binding_Sonic_Std(b *testing.B) {
_, _ = Encode(&_BindingValue, 0)
b.SetBytes(int64(len(TwitterJson)))
b.ResetTimer()
var buf []byte
for i := 0; i < b.N; i++ {
buf, _ = Encode(&_BindingValue, SortMapKeys | EscapeHTML)
}
_ = buf
}
func BenchmarkEncoder_Binding_JsonIter(b *testing.B) {
_, _ = jsoniter.Marshal(&_BindingValue)
b.SetBytes(int64(len(TwitterJson)))
@ -423,3 +443,27 @@ func BenchmarkEncoder_Parallel_Binding_StdLib(b *testing.B) {
}
})
}
func BenchmarkHTMLEscape_Sonic(b *testing.B) {
jsonByte := []byte(TwitterJson)
b.SetBytes(int64(len(TwitterJson)))
b.ResetTimer()
var buf []byte
for i := 0; i < b.N; i++ {
buf = HTMLEscape(nil, jsonByte)
}
_ = buf
}
func BenchmarkHTMLEscape_StdLib(b *testing.B) {
jsonByte := []byte(TwitterJson)
b.SetBytes(int64(len(TwitterJson)))
b.ResetTimer()
var buf []byte
for i := 0; i < b.N; i++ {
out := bytes.NewBuffer(make([]byte, 0, len(TwitterJson) * 6 / 5))
json.HTMLEscape(out, jsonByte)
buf = out.Bytes()
}
_ = buf
}

View file

@ -54,6 +54,11 @@ func __lspace(sp unsafe.Pointer, nb int, off int) (ret int)
//goland:noinspection GoUnusedParameter
func __quote(sp unsafe.Pointer, nb int, dp unsafe.Pointer, dn *int, flags uint64) (ret int)
//go:nosplit
//go:noescape
//goland:noinspection GoUnusedParameter
func __html_escape(sp unsafe.Pointer, nb int, dp unsafe.Pointer, dn *int) (ret int)
//go:nosplit
//go:noescape
//goland:noinspection GoUnusedParameter

File diff suppressed because it is too large Load diff

View file

@ -208,6 +208,31 @@ func TestNative_UnquoteUnicodeReplacement(t *testing.T) {
assert.Equal(t, "hello\ufffd\ufffdworld", string(d))
}
func TestNative_HTMLEscape(t *testing.T) {
s := "hello\u2029\u2028<&>world"
d := make([]byte, 256)
dp := (*rt.GoSlice)(unsafe.Pointer(&d))
sp := (*rt.GoString)(unsafe.Pointer(&s))
rv := __html_escape(sp.Ptr, sp.Len, dp.Ptr, &dp.Len)
if rv < 0 {
require.NoError(t, types.ParsingError(-rv))
}
assert.Equal(t, len(s), rv)
assert.Equal(t, 40, len(d))
assert.Equal(t, `hello\u2029\u2028\u003c\u0026\u003eworld`, string(d))
}
func TestNative_HTMLEscapeNoMem(t *testing.T) {
s := "hello\u2029\u2028<&>world"
d := make([]byte, 10)
dp := (*rt.GoSlice)(unsafe.Pointer(&d))
sp := (*rt.GoString)(unsafe.Pointer(&s))
rv := __html_escape(sp.Ptr, sp.Len, dp.Ptr, &dp.Len)
assert.Equal(t, -8, rv)
assert.Equal(t, 5, len(d))
assert.Equal(t, `hello`, string(d))
}
func TestNative_Vstring(t *testing.T) {
var v types.JsonState
i := 0

View file

@ -10,28 +10,30 @@ func __native_entry__() uintptr
var (
_subr__f64toa = __native_entry__() + 630
_subr__html_escape = __native_entry__() + 8160
_subr__i64toa = __native_entry__() + 3642
_subr__lspace = __native_entry__() + 301
_subr__lzero = __native_entry__() + 13
_subr__quote = __native_entry__() + 4955
_subr__skip_array = __native_entry__() + 16074
_subr__skip_object = __native_entry__() + 16109
_subr__skip_one = __native_entry__() + 14295
_subr__skip_array = __native_entry__() + 17223
_subr__skip_object = __native_entry__() + 17258
_subr__skip_one = __native_entry__() + 15444
_subr__u64toa = __native_entry__() + 3735
_subr__unquote = __native_entry__() + 5888
_subr__value = __native_entry__() + 9657
_subr__vnumber = __native_entry__() + 12453
_subr__vsigned = __native_entry__() + 13767
_subr__vstring = __native_entry__() + 11418
_subr__vunsigned = __native_entry__() + 14026
_subr__unquote = __native_entry__() + 6005
_subr__value = __native_entry__() + 10806
_subr__vnumber = __native_entry__() + 13602
_subr__vsigned = __native_entry__() + 14916
_subr__vstring = __native_entry__() + 12567
_subr__vunsigned = __native_entry__() + 15175
)
const (
_stack__f64toa = 120
_stack__html_escape = 72
_stack__i64toa = 24
_stack__lspace = 8
_stack__lzero = 8
_stack__quote = 64
_stack__quote = 80
_stack__skip_array = 136
_stack__skip_object = 136
_stack__skip_one = 136
@ -46,6 +48,7 @@ const (
var (
_ = _subr__f64toa
_ = _subr__html_escape
_ = _subr__i64toa
_ = _subr__lspace
_ = _subr__lzero
@ -64,6 +67,7 @@ var (
const (
_ = _stack__f64toa
_ = _stack__html_escape
_ = _stack__i64toa
_ = _stack__lspace
_ = _stack__lzero

View file

@ -54,6 +54,11 @@ func __lspace(sp unsafe.Pointer, nb int, off int) (ret int)
//goland:noinspection GoUnusedParameter
func __quote(sp unsafe.Pointer, nb int, dp unsafe.Pointer, dn *int, flags uint64) (ret int)
//go:nosplit
//go:noescape
//goland:noinspection GoUnusedParameter
func __html_escape(sp unsafe.Pointer, nb int, dp unsafe.Pointer, dn *int) (ret int)
//go:nosplit
//go:noescape
//goland:noinspection GoUnusedParameter

File diff suppressed because it is too large Load diff

View file

@ -208,6 +208,31 @@ func TestNative_UnquoteUnicodeReplacement(t *testing.T) {
assert.Equal(t, "hello\ufffd\ufffdworld", string(d))
}
func TestNative_HTMLEscape(t *testing.T) {
s := "hello\u2029\u2028<&>world"
d := make([]byte, 256)
dp := (*rt.GoSlice)(unsafe.Pointer(&d))
sp := (*rt.GoString)(unsafe.Pointer(&s))
rv := __html_escape(sp.Ptr, sp.Len, dp.Ptr, &dp.Len)
if rv < 0 {
require.NoError(t, types.ParsingError(-rv))
}
assert.Equal(t, len(s), rv)
assert.Equal(t, 40, len(d))
assert.Equal(t, `hello\u2029\u2028\u003c\u0026\u003eworld`, string(d))
}
func TestNative_HTMLEscapeNoMem(t *testing.T) {
s := "hello\u2029\u2028<&>world"
d := make([]byte, 10)
dp := (*rt.GoSlice)(unsafe.Pointer(&d))
sp := (*rt.GoString)(unsafe.Pointer(&s))
rv := __html_escape(sp.Ptr, sp.Len, dp.Ptr, &dp.Len)
assert.Equal(t, -8, rv)
assert.Equal(t, 5, len(d))
assert.Equal(t, `hello`, string(d))
}
func TestNative_Vstring(t *testing.T) {
var v types.JsonState
i := 0

View file

@ -10,28 +10,30 @@ func __native_entry__() uintptr
var (
_subr__f64toa = __native_entry__() + 903
_subr__html_escape = __native_entry__() + 9535
_subr__i64toa = __native_entry__() + 3915
_subr__lspace = __native_entry__() + 429
_subr__lzero = __native_entry__() + 13
_subr__quote = __native_entry__() + 5328
_subr__skip_array = __native_entry__() + 19163
_subr__skip_object = __native_entry__() + 19198
_subr__skip_one = __native_entry__() + 16306
_subr__skip_array = __native_entry__() + 21058
_subr__skip_object = __native_entry__() + 21093
_subr__skip_one = __native_entry__() + 18201
_subr__u64toa = __native_entry__() + 4008
_subr__unquote = __native_entry__() + 7125
_subr__value = __native_entry__() + 11812
_subr__vnumber = __native_entry__() + 14464
_subr__vsigned = __native_entry__() + 15778
_subr__vstring = __native_entry__() + 13587
_subr__vunsigned = __native_entry__() + 16037
_subr__unquote = __native_entry__() + 7080
_subr__value = __native_entry__() + 13707
_subr__vnumber = __native_entry__() + 16359
_subr__vsigned = __native_entry__() + 17673
_subr__vstring = __native_entry__() + 15482
_subr__vunsigned = __native_entry__() + 17932
)
const (
_stack__f64toa = 120
_stack__html_escape = 56
_stack__i64toa = 24
_stack__lspace = 8
_stack__lzero = 8
_stack__quote = 80
_stack__quote = 64
_stack__skip_array = 128
_stack__skip_object = 128
_stack__skip_one = 128
@ -46,6 +48,7 @@ const (
var (
_ = _subr__f64toa
_ = _subr__html_escape
_ = _subr__i64toa
_ = _subr__lspace
_ = _subr__lzero
@ -64,6 +67,7 @@ var (
const (
_ = _stack__f64toa
_ = _stack__html_escape
_ = _stack__i64toa
_ = _stack__lspace
_ = _stack__lzero

View file

@ -68,6 +68,11 @@ func Quote(s unsafe.Pointer, nb int, dp unsafe.Pointer, dn *int, flags uint64) i
//goland:noinspection GoUnusedParameter
func Unquote(s unsafe.Pointer, nb int, dp unsafe.Pointer, ep *int, flags uint64) int
//go:nosplit
//go:noescape
//goland:noinspection GoUnusedParameter
func HTMLEscape(s unsafe.Pointer, nb int, dp unsafe.Pointer, dn *int) int
//go:nosplit
//go:noescape
//goland:noinspection GoUnusedParameter

View file

@ -36,6 +36,12 @@ TEXT ·Unquote(SB), NOSPLIT, $0 - 48
JMP github·combytedancesonicinternalnativeavx2·__unquote(SB)
JMP github·combytedancesonicinternalnativeavx·__unquote(SB)
TEXT ·HTMLEscape(SB), NOSPLIT, $0 - 40
CMPB github·combytedancesonicinternalcpu·HasAVX2(SB), $0
JE 2(PC)
JMP github·combytedancesonicinternalnativeavx2·__html_escape(SB)
JMP github·combytedancesonicinternalnativeavx·__html_escape(SB)
TEXT ·Value(SB), NOSPLIT, $0 - 48
CMPB github·combytedancesonicinternalcpu·HasAVX2(SB), $0
JE 2(PC)

View file

@ -52,6 +52,11 @@ func __lspace(sp unsafe.Pointer, nb int, off int) (ret int)
//goland:noinspection GoUnusedParameter
func __quote(sp unsafe.Pointer, nb int, dp unsafe.Pointer, dn *int, flags uint64) (ret int)
//go:nosplit
//go:noescape
//goland:noinspection GoUnusedParameter
func __html_escape(sp unsafe.Pointer, nb int, dp unsafe.Pointer, dn *int) (ret int)
//go:nosplit
//go:noescape
//goland:noinspection GoUnusedParameter

View file

@ -206,6 +206,31 @@ func TestNative_UnquoteUnicodeReplacement(t *testing.T) {
assert.Equal(t, "hello\ufffd\ufffdworld", string(d))
}
func TestNative_HTMLEscape(t *testing.T) {
s := "hello\u2029\u2028<&>world"
d := make([]byte, 256)
dp := (*rt.GoSlice)(unsafe.Pointer(&d))
sp := (*rt.GoString)(unsafe.Pointer(&s))
rv := __html_escape(sp.Ptr, sp.Len, dp.Ptr, &dp.Len)
if rv < 0 {
require.NoError(t, types.ParsingError(-rv))
}
assert.Equal(t, len(s), rv)
assert.Equal(t, 40, len(d))
assert.Equal(t, `hello\u2029\u2028\u003c\u0026\u003eworld`, string(d))
}
func TestNative_HTMLEscapeNoMem(t *testing.T) {
s := "hello\u2029\u2028<&>world"
d := make([]byte, 10)
dp := (*rt.GoSlice)(unsafe.Pointer(&d))
sp := (*rt.GoString)(unsafe.Pointer(&s))
rv := __html_escape(sp.Ptr, sp.Len, dp.Ptr, &dp.Len)
assert.Equal(t, -8, rv)
assert.Equal(t, 5, len(d))
assert.Equal(t, `hello`, string(d))
}
func TestNative_Vstring(t *testing.T) {
var v types.JsonState
i := 0

View file

@ -106,6 +106,7 @@ size_t lspace(const char *sp, size_t nb, size_t p);
ssize_t quote(const char *sp, ssize_t nb, char *dp, ssize_t *dn, uint64_t flags);
ssize_t unquote(const char *sp, ssize_t nb, char *dp, ssize_t *ep, uint64_t flags);
ssize_t html_escape(const char *sp, ssize_t nb, char *dp, ssize_t *dn);
long value(const char *s, size_t n, long p, JsonState *ret, int allow_control);
void vstring(const GoString *src, long *p, JsonState *ret);

View file

@ -97,7 +97,17 @@ static const quoted_t _DoubleQuoteTab[256] = {
['\\' ] = { .n = 4, .s = "\\\\\\\\" },
};
static inline void memcpy_p8(char *dp, const char *sp, size_t nb) {
static const quoted_t _HtmlQuoteTab[256] = {
['<'] = { .n = 6, .s = "\\u003c" },
['>'] = { .n = 6, .s = "\\u003e" },
['&'] = { .n = 6, .s = "\\u0026" },
// \u2028 and \u2029 is [E2 80 A8] and [E2 80 A9]
[0xe2] = { .n = 0, .s = {0} },
[0xa8] = { .n = 6, .s = "\\u2028" },
[0xa9] = { .n = 6, .s = "\\u2029" },
};
static inline void memcpy_p8(char *dp, const char *sp, ssize_t nb) {
if (nb >= 4) { *(uint32_t *)dp = *(const uint32_t *)sp; sp += 4, dp += 4, nb -= 4; }
if (nb >= 2) { *(uint16_t *)dp = *(const uint16_t *)sp; sp += 2, dp += 2, nb -= 2; }
if (nb >= 1) { *dp = *sp; }
@ -621,3 +631,183 @@ ssize_t unquote(const char *sp, ssize_t nb, char *dp, ssize_t *ep, uint64_t flag
/* calculate the result length */
return dp + nb - p;
}
static inline __m128i _mm_find_html(__m128i vv) {
__m128i e1 = _mm_cmpeq_epi8 (vv, _mm_set1_epi8('<'));
__m128i e2 = _mm_cmpeq_epi8 (vv, _mm_set1_epi8('>'));
__m128i e3 = _mm_cmpeq_epi8 (vv, _mm_set1_epi8('&'));
__m128i e4 = _mm_cmpeq_epi8 (vv, _mm_set1_epi8('\xe2'));
__m128i r1 = _mm_or_si128 (e1, e2);
__m128i r2 = _mm_or_si128 (e3, e4);
__m128i rv = _mm_or_si128 (r1, r2);
return rv;
}
#if USE_AVX2
static inline __m256i _mm256_find_html(__m256i vv) {
__m256i e1 = _mm256_cmpeq_epi8 (vv, _mm256_set1_epi8('<'));
__m256i e2 = _mm256_cmpeq_epi8 (vv, _mm256_set1_epi8('>'));
__m256i e3 = _mm256_cmpeq_epi8 (vv, _mm256_set1_epi8('&'));
__m256i e4 = _mm256_cmpeq_epi8 (vv, _mm256_set1_epi8('\xe2'));
__m256i r1 = _mm256_or_si256 (e1, e2);
__m256i r2 = _mm256_or_si256 (e3, e4);
__m256i rv = _mm256_or_si256 (r1, r2);
return rv;
}
#endif
static inline ssize_t memcchr_html_quote(const char *sp, ssize_t nb, char *dp, ssize_t dn) {
uint32_t mm;
const char * ss = sp;
#if USE_AVX2
/* 32-byte loop, full store */
while (nb >= 32 && dn >= 32) {
__m256i vv = _mm256_loadu_si256 ((const void *)sp);
__m256i rv = _mm256_find_html (vv);
_mm256_storeu_si256 ((void *)dp, vv);
/* check for matches */
if ((mm = _mm256_movemask_epi8(rv)) != 0) {
return sp - ss + __builtin_ctz(mm);
}
/* move to next block */
sp += 32;
dp += 32;
nb -= 32;
dn -= 32;
}
/* 32-byte test, partial store */
if (nb >= 32) {
__m256i vv = _mm256_loadu_si256 ((const void *)sp);
__m256i rv = _mm256_find_html (vv);
uint32_t mv = _mm256_movemask_epi8 (rv);
uint32_t fv = __builtin_ctzll ((uint64_t)mv | 0x0100000000);
/* copy at most `dn` characters */
if (fv <= dn) {
memcpy_p32(dp, sp, fv);
return sp - ss + fv;
} else {
memcpy_p32(dp, sp, dn);
return -(sp - ss + dn) - 1;
}
}
/* clear upper half to avoid AVX-SSE transition penalty */
_mm256_zeroupper();
#endif
/* 16-byte loop, full store */
while (nb >= 16 && dn >= 16) {
__m128i vv = _mm_loadu_si128 ((const void *)sp);
__m128i rv = _mm_find_html (vv);
_mm_storeu_si128 ((void *)dp, vv);
/* check for matches */
if ((mm = _mm_movemask_epi8(rv)) != 0) {
return sp - ss + __builtin_ctz(mm);
}
/* move to next block */
sp += 16;
dp += 16;
nb -= 16;
dn -= 16;
}
/* 16-byte test, partial store */
if (nb >= 16) {
__m128i vv = _mm_loadu_si128 ((const void *)sp);
__m128i rv = _mm_find_html (vv);
uint32_t mv = _mm_movemask_epi8 (rv);
uint32_t fv = __builtin_ctz (mv | 0x010000);
/* copy at most `dn` characters */
if (fv <= dn) {
memcpy_p16(dp, sp, fv);
return sp - ss + fv;
} else {
memcpy_p16(dp, sp, dn);
return -(sp - ss + dn) - 1;
}
}
/* handle the remaining bytes with scalar code */
while (nb > 0 && dn > 0) {
if (*sp == '<' || *sp == '>' || *sp == '&' || *sp == '\xe2') {
return sp - ss;
} else {
dn--, nb--;
*dp++ = *sp++;
}
}
/* check for dest buffer */
if (nb == 0) {
return sp - ss;
} else {
return -(sp - ss) - 1;
}
}
ssize_t html_escape(const char *sp, ssize_t nb, char *dp, ssize_t *dn) {
ssize_t nd = *dn;
const char * ds = dp;
const char * ss = sp;
const quoted_t * tab = _HtmlQuoteTab;
/* find the special characters, copy on the fly */
while (nb != 0) {
int nc = 0;
uint8_t ch = 0;
ssize_t rb = memcchr_html_quote(sp, nb, dp, nd);
/* not enough buffer space */
if (rb < 0) {
*dn = dp - ds - rb - 1;
return -(sp - ss - rb - 1) - 1;
}
/* skip already copied bytes */
sp += rb;
dp += rb;
nb -= rb;
nd -= rb;
/* stop if already finished */
if (nb <= 0) {
break;
}
/* check for \u2028 and \u2029, [e2 80 a8] and [e2 80 a9] */
if (nb >= 3 && 0xa880e2 == (*(uint32_t *)sp & 0xfeffff)) {
sp += 2;
nb -= 2;
}
/* get the escape entry, handle consecutive quotes */
ch = * (uint8_t*) sp;
nc = tab[ch].n;
/* check for buffer space */
if (nd < nc) {
*dn = dp - ds;
return -(sp - ss) - 1;
}
/* copy the quoted value */
memcpy_p8(dp, tab[ch].s, nc);
sp++;
nb--;
dp += nc;
nd -= nc;
}
/* all done */
*dn = dp - ds;
return sp - ss;
}