Skip to content

Commit

Permalink
Move optimized ucs22str function to 64bit only (#43)
Browse files Browse the repository at this point in the history
* separate non-32-bit-compatible code

* omit -race on 32bit build
  • Loading branch information
shueybubbles authored Aug 4, 2022
1 parent c33ed63 commit 63479d5
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 184 deletions.
17 changes: 13 additions & 4 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ environment:
SQLPASSWORD: Password12!
DATABASE: test
GOVERSION: 113
RACE: -race -cpu 4
matrix:
- GOVERSION: 19
SQLINSTANCE: SQL2017
- GOVERSION: 110
SQLINSTANCE: SQL2017
- GOVERSION: 111
Expand All @@ -40,7 +39,17 @@ environment:
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
GOVERSION: 116
SQLINSTANCE: SQL2017

- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
GOVERSION: 117
SQLINSTANCE: SQL2017
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
GOVERSION: 118
SQLINSTANCE: SQL2017
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
GOVERSION: 118-x86
SQLINSTANCE: SQL2017
GOARCH: 386
RACE:
install:
- set GOROOT=c:\go%GOVERSION%
- set PATH=%GOPATH%\bin;%GOROOT%\bin;%PATH%
Expand All @@ -63,5 +72,5 @@ before_test:
- pip install codecov

test_script:
- go test -race -cpu 4 -coverprofile=coverage.txt -covermode=atomic
- go test %RACE% -coverprofile=coverage.txt -covermode=atomic
- codecov -f coverage.txt
4 changes: 2 additions & 2 deletions bulkcopy_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//go:build go1.9
// +build go1.9
//go:build go1.9 && !386
// +build go1.9,!386

package mssql

Expand Down
43 changes: 43 additions & 0 deletions queries_go19_amd64_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package mssql

import (
"fmt"
"testing"
"time"
)

func TestDateTimeParam19(t *testing.T) {
conn, logger := open(t)
defer conn.Close()
logger.StopLogging()

// testing DateTime1, only supported on go 1.9
var emptydate time.Time
mindate1 := time.Date(1753, 1, 1, 0, 0, 0, 0, time.UTC)
maxdate1 := time.Date(9999, 12, 31, 23, 59, 59, 997000000, time.UTC)
testdates1 := []DateTime1{
DateTime1(mindate1),
DateTime1(maxdate1),
DateTime1(time.Date(1752, 12, 31, 23, 59, 59, 997000000, time.UTC)), // just a little below minimum date
DateTime1(time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC)), // just a little over maximum date
DateTime1(emptydate),
}

for _, test := range testdates1 {
t.Run(fmt.Sprintf("Test datetime for %v", test), func(t *testing.T) {
var res time.Time
expected := time.Time(test)
queryParamRoundTrip(conn, test, &res)
// clip value
if expected.Before(mindate1) {
expected = mindate1
}
if expected.After(maxdate1) {
expected = maxdate1
}
if expected.Sub(res) != 0 {
t.Errorf("expected: '%s', got: '%s' delta: %d", expected, res, expected.Sub(res))
}
})
}
}
36 changes: 0 additions & 36 deletions queries_go19_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1012,42 +1012,6 @@ with
}
}

func TestDateTimeParam19(t *testing.T) {
conn, logger := open(t)
defer conn.Close()
logger.StopLogging()

// testing DateTime1, only supported on go 1.9
var emptydate time.Time
mindate1 := time.Date(1753, 1, 1, 0, 0, 0, 0, time.UTC)
maxdate1 := time.Date(9999, 12, 31, 23, 59, 59, 997000000, time.UTC)
testdates1 := []DateTime1{
DateTime1(mindate1),
DateTime1(maxdate1),
DateTime1(time.Date(1752, 12, 31, 23, 59, 59, 997000000, time.UTC)), // just a little below minimum date
DateTime1(time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC)), // just a little over maximum date
DateTime1(emptydate),
}

for _, test := range testdates1 {
t.Run(fmt.Sprintf("Test datetime for %v", test), func(t *testing.T) {
var res time.Time
expected := time.Time(test)
queryParamRoundTrip(conn, test, &res)
// clip value
if expected.Before(mindate1) {
expected = mindate1
}
if expected.After(maxdate1) {
expected = maxdate1
}
if expected.Sub(res) != 0 {
t.Errorf("expected: '%s', got: '%s' delta: %d", expected, res, expected.Sub(res))
}
})
}
}

func TestReturnStatus(t *testing.T) {
conn, logger := open(t)
defer conn.Close()
Expand Down
142 changes: 0 additions & 142 deletions tds.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ import (
"io"
"io/ioutil"
"net"
"reflect"
"sort"
"strconv"
"strings"
"time"
"unicode/utf16"
"unicode/utf8"
"unsafe"

"github.com/microsoft/go-mssqldb/msdsn"
)
Expand Down Expand Up @@ -476,146 +474,6 @@ const (
mask16 uint16 = 0xFF80
)

func ucs22str(s []byte) (string, error) {
if len(s)%2 != 0 {
return "", fmt.Errorf("illegal UCS2 string length: %d", len(s))
}

// allocate a buffer which we will attempt to copy ascii into, optimistically, as we validate
buf := make([]byte, len(s)/2)
useFastPath := true

// how many 8 byte chunks are in the input buffer
nlen8 := len(s) & 0xFFFFFFF8
// our read and write offsets into the buffers
var (
readIndex int
writeIndex int
)

// step through in 8 byte chunks.
for readIndex = 0; readIndex < nlen8; readIndex += 8 {

// dereference directly into the array as uint64s
ui64 := *(*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(&s[0])) + uintptr(readIndex)))

// mask the entire 64 bit region and check for
// 1) even bytes > 0
// 2) odd bytes with their high bit set
// the mask for this is FF80....
if ui64&mask64 > 0 {
// if we find a value once masked, we have to take the slow path as this is not an ascii string
useFastPath = false
break
}

// we are ok to read out the 4 odd bytes and remove the empty even bytes
var ui32 uint32 = 0
ui32 |= uint32(byte(ui64))
ui64 = ui64 >> 8

ui32 |= uint32(uint16(ui64))
ui64 = ui64 >> 8

ui32 |= uint32(ui64 & 0xFF0000)
ui64 = ui64 >> 8
ui32 |= uint32(ui64 & 0xFF000000)

// write the new 32 bit value to the destination buffer
ptrui32 := ((*uint32)(unsafe.Pointer(uintptr(unsafe.Pointer(&buf[0])) + uintptr(writeIndex))))
*ptrui32 = ui32

// step forward four bytes in the destinaiton buffer
writeIndex += 4
}

// can we continue reading on the fast ascii path?
if useFastPath {
// we have now dealt with all the avalable 8 byte chunks, we have at most 7 bytes remaining.

// have we got at least 4 bytes remaining to be read?
if len(s)-readIndex >= 4 {
// deal with the next 32 bit region

// read 32 bits from the current read position in the source slice
ui32 := *(*uint32)(unsafe.Pointer(uintptr(unsafe.Pointer(&s[0])) + uintptr(readIndex)))

// mask the 32 bit value as above. again, if we find a value
// this is not ascii and we need to fall back to the slow path
// this time with a 32 bit mask
if ui32&mask32 > 0 {
// we have found non ascii text and must fallback
useFastPath = false
} else {

// read the two odd positions bytes and write as a single 16 bit value
var ui16 uint16 = 0
ui16 |= uint16(byte(ui32))
ui32 = ui32 >> 8

ui16 |= uint16(ui32)

ptrui16 := ((*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(&buf[0])) + uintptr((writeIndex)))))
*ptrui16 = ui16

// step forward the read and write positions.
readIndex += 4
writeIndex += 2
}
}

// Are we still on the fast path?
if useFastPath {
// have we got at least 2 bytes remaining to be read?
// actually we can only have at most 2 bytes at this point
// since we know the source buffer has even length.
if len(s)-readIndex >= 2 {

// read 2 bytes
ui16 := *(*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(&s[0])) + uintptr(readIndex)))

// mask again, but only 16bits
if ui16&mask16 == 0 {
// manually pull out the low byte and write to our destination buffer
buf[writeIndex] = byte(ui16 & 0xFF)
// we have now successfully read the entire ascii buffer and can convert to a string
return *(*string)(unsafe.Pointer(&buf)), nil
}
} else {
// there were no further bytes to read, but we have successfully read the ascii
// and can convert to a string
return *(*string)(unsafe.Pointer(&buf)), nil
}
}
}

// one of the above checks has found non ascii values in the buffer, either
// a high bit set in an odd byte or any non zero in an even byte.
// we fall back to a slower conversion here.

// we can reuse the underlying array and create our own uint16 slice here
// because utf16.Decode allocates a new buffer and only reads its input.

// declare a real uint16 slice so that the compiler can keep track of
// the underlying memory as we transfer & convert it.
// This is to ensure that the GC does not prematurely collect our data.
var uint16slice []uint16

uint16Header := (*reflect.SliceHeader)(unsafe.Pointer(&uint16slice))
sourceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&s))

uint16Header.Data = sourceHeader.Data
// it is important to reference s after the assignment of the Data
// pointer to make sure that s is not garbage collected before
// we have another reference to the data.
uint16Header.Len = len(s) / 2 // the output is half the length in bytes
uint16Header.Cap = uint16Header.Len // the capacity is also half the number of bytes

// decode the uint16s as utf-16 and return a string.
// After this point both s and uint16slice can be garbage collected.
return string(utf16.Decode(uint16slice)), nil
}

func manglePassword(password string) []byte {
var ucs2password []byte = str2ucs2(password)
for i, ch := range ucs2password {
Expand Down
Loading

0 comments on commit 63479d5

Please sign in to comment.