Repository: segmentio/parquet-go
Pull request #240 (2022-06-15 23:20:05)

Optimize delta decoding (part 5)
This PR contributes to #226 by adding optimizations for decoding of 64 bit delta-encoded values.

I left comments inline, and focused on bit-packing between sizes 1 to 32 where the algorithm is a bit simpler than when values span across more than 32 bits. In practice, our use cases for delta-encoding on 64 bit values to have deltas <= MaxInt32 (e.g. encoding nearby timestamps which microsecond precision), we can revisit later and add specializations for the upper sizes if we measure it being a bottleneck.

I took a slightly different approach than in #230, I left documentation in the code explaining why.

```
name                     old time/op    new time/op     delta
UnpackInt64/bitWidth=1      163ns ± 0%       23ns ± 0%   -86.01%  (p=0.000 n=10+9)
UnpackInt64/bitWidth=8      164ns ± 0%       22ns ± 0%   -86.30%  (p=0.000 n=10+9)
UnpackInt64/bitWidth=10     186ns ± 0%       23ns ± 2%   -87.52%  (p=0.000 n=10+10)
UnpackInt64/bitWidth=20     204ns ± 1%       23ns ± 0%   -88.65%  (p=0.000 n=10+10)
UnpackInt64/bitWidth=30     241ns ± 1%       23ns ± 0%   -90.29%  (p=0.000 n=10+10)
UnpackInt64/bitWidth=40     252ns ± 1%      255ns ± 0%    +1.46%  (p=0.000 n=10+6)

name                     old speed      new speed       delta
UnpackInt64/bitWidth=1   3.14GB/s ± 0%  22.42GB/s ± 0%  +614.85%  (p=0.000 n=10+9)
UnpackInt64/bitWidth=8   3.13GB/s ± 0%  22.85GB/s ± 0%  +629.74%  (p=0.000 n=10+9)
UnpackInt64/bitWidth=10  2.76GB/s ± 0%  22.10GB/s ± 2%  +700.99%  (p=0.000 n=10+10)
UnpackInt64/bitWidth=20  2.51GB/s ± 1%  22.15GB/s ± 0%  +781.28%  (p=0.000 n=10+10)
UnpackInt64/bitWidth=30  2.13GB/s ± 1%  21.92GB/s ± 0%  +930.28%  (p=0.000 n=10+10)
UnpackInt64/bitWidth=40  2.03GB/s ± 1%   2.01GB/s ± 0%    -1.45%  (p=0.000 n=10+6)
```

```
name                                  old time/op  new time/op  delta
MergeFiles/INT64/groups=2,rows=20000  9.94µs ± 1%  7.48µs ± 1%  -24.76%  (p=0.000 n=9+9)

name                                  old row/s    new row/s    delta
MergeFiles/INT64/groups=2,rows=20000   97.4M ± 1%  129.4M ± 1%  +32.92%  (p=0.000 n=9+9)
```


Patch:
diff --git a/bloom.go b/bloom.go
index b2ea7ec..8918e54 100644
--- a/bloom.go
+++ b/bloom.go
@@ -177,7 +177,7 @@ func (splitBlockEncoding) EncodeByteArray(dst, src []byte) ([]byte, error) {
 	filter := bloom.MakeSplitBlockFilter(dst)
 	buffer := make([]uint64, 0, filterEncodeBufferSize)
 
-	err := plain.RangeByteArrays(src, func(value []byte) error {
+	err := plain.RangeByteArray(src, func(value []byte) error {
 		if len(buffer) == cap(buffer) {
 			filter.InsertBulk(buffer)
 			buffer = buffer[:0]
diff --git a/column_buffer.go b/column_buffer.go
index dfb07c3..f3587ac 100644
--- a/column_buffer.go
+++ b/column_buffer.go
@@ -1507,7 +1507,7 @@ func (col *byteArrayColumnBuffer) WriteByteArrays(values []byte) (int, error) {
 func (col *byteArrayColumnBuffer) writeByteArrays(values []byte) (count, bytes int, err error) {
 	baseCount, baseBytes := len(col.offsets), len(col.values)
 
-	err = plain.RangeByteArrays(values, func(value []byte) error {
+	err = plain.RangeByteArray(values, func(value []byte) error {
 		col.append(unsafecast.BytesToString(value))
 		return nil
 	})
diff --git a/column_index.go b/column_index.go
index efbc9eb..8df2702 100644
--- a/column_index.go
+++ b/column_index.go
@@ -693,13 +693,13 @@ func isMaxByteArrayValue(value []byte) bool {
 
 func splitByteArrays(data []byte) [][]byte {
 	length := 0
-	plain.RangeByteArrays(data, func([]byte) error {
+	plain.RangeByteArray(data, func([]byte) error {
 		length++
 		return nil
 	})
 	buffer := make([]byte, 0, len(data)-(4*length))
 	values := make([][]byte, 0, length)
-	plain.RangeByteArrays(data, func(value []byte) error {
+	plain.RangeByteArray(data, func(value []byte) error {
 		offset := len(buffer)
 		buffer = append(buffer, value...)
 		values = append(values, buffer[offset:])
diff --git a/encoding/delta/binary_packed.go b/encoding/delta/binary_packed.go
index 6e47e84..3228d57 100644
--- a/encoding/delta/binary_packed.go
+++ b/encoding/delta/binary_packed.go
@@ -323,8 +323,8 @@ func decodeInt32(dst, src []byte) ([]byte, []byte, error) {
 					miniBlockData = miniBlockData[:miniBlockSize]
 				}
 				src = src[len(miniBlockData):]
-				if cap(miniBlockData) < miniBlockSize+bitpack.Padding {
-					miniBlockTemp = resize(miniBlockTemp[:0], miniBlockSize+bitpack.Padding)
+				if cap(miniBlockData) < miniBlockSize+bitpack.PaddingInt32 {
+					miniBlockTemp = resize(miniBlockTemp[:0], miniBlockSize+bitpack.PaddingInt32)
 					miniBlockData = miniBlockTemp[:copy(miniBlockTemp, miniBlockData)]
 				}
 				miniBlockData = miniBlockData[:miniBlockSize]
@@ -386,8 +386,8 @@ func decodeInt64(dst, src []byte) ([]byte, []byte, error) {
 					miniBlockData = src[:miniBlockSize]
 				}
 				src = src[len(miniBlockData):]
-				if len(miniBlockData) < miniBlockSize+bitpack.Padding {
-					miniBlockTemp = resize(miniBlockTemp[:0], miniBlockSize+bitpack.Padding)
+				if len(miniBlockData) < miniBlockSize+bitpack.PaddingInt64 {
+					miniBlockTemp = resize(miniBlockTemp[:0], miniBlockSize+bitpack.PaddingInt64)
 					miniBlockData = miniBlockTemp[:copy(miniBlockTemp, miniBlockData)]
 				}
 				miniBlockData = miniBlockData[:miniBlockSize]
diff --git a/encoding/plain/plain.go b/encoding/plain/plain.go
index 43191f1..cd4aeac 100644
--- a/encoding/plain/plain.go
+++ b/encoding/plain/plain.go
@@ -71,7 +71,7 @@ func (e *Encoding) EncodeDouble(dst, src []byte) ([]byte, error) {
 }
 
 func (e *Encoding) EncodeByteArray(dst []byte, src []byte) ([]byte, error) {
-	if err := ValidateByteArrays(src); err != nil {
+	if err := ValidateByteArray(src); err != nil {
 		return dst[:0], encoding.Error(e, err)
 	}
 	return append(dst[:0], src...), nil
@@ -124,7 +124,7 @@ func (e *Encoding) DecodeDouble(dst, src []byte) ([]byte, error) {
 }
 
 func (e *Encoding) DecodeByteArray(dst, src []byte) ([]byte, error) {
-	if err := ValidateByteArrays(src); err != nil {
+	if err := ValidateByteArray(src); err != nil {
 		return dst[:0], encoding.Error(e, err)
 	}
 	return append(dst[:0], src...), nil
@@ -246,8 +246,8 @@ const (
 	errTooLarge
 )
 
-func ValidateByteArrays(b []byte) error {
-	switch validateByteArrays(b) {
+func ValidateByteArray(b []byte) error {
+	switch validateByteArray(b) {
 	case errTooShort:
 		return ErrTooShort(len(b))
 	case errTooLarge:
@@ -257,7 +257,7 @@ func ValidateByteArrays(b []byte) error {
 	}
 }
 
-func RangeByteArrays(b []byte, do func([]byte) error) (err error) {
+func RangeByteArray(b []byte, do func([]byte) error) (err error) {
 	for len(b) > 0 {
 		var v []byte
 		if v, b, err = NextByteArray(b); err != nil {
diff --git a/encoding/plain/plain_amd64.go b/encoding/plain/plain_amd64.go
index 96efec1..b537771 100644
--- a/encoding/plain/plain_amd64.go
+++ b/encoding/plain/plain_amd64.go
@@ -3,4 +3,4 @@
 package plain
 
 //go:noescape
-func validateByteArrays([]byte) status
+func validateByteArray([]byte) status
diff --git a/encoding/plain/plain_amd64.s b/encoding/plain/plain_amd64.s
index 748470c..5f29fd7 100644
--- a/encoding/plain/plain_amd64.s
+++ b/encoding/plain/plain_amd64.s
@@ -2,45 +2,40 @@
 
 #include "textflag.h"
 
-// validateByteArrays is implemented in assembly because it is the bottleneck in
+// validateByteArray is implemented in assembly because it is the bottleneck in
 // many cases when encoding byte arrays. This version of the function yields
 // ~50% better throughput than the code generated by the Go compiler.
 //
-// func validateByteArrays([]byte) status
-TEXT ·validateByteArrays(SB), NOSPLIT, $0-32
+// func validateByteArray([]byte) status
+TEXT ·validateByteArray(SB), NOSPLIT, $0-32
     MOVQ arg_base+0(FP), AX
     MOVQ arg_len+8(FP), BX
 
     CMPQ BX, $0
     JE done
 
-    XORQ SI, SI
-loop:
-    MOVQ BX, CX
-    SUBQ SI, CX
-    CMPQ CX, $4
+    CMPQ BX, $4
     JB errTooShort
 
-    MOVLQZX (AX)(SI*1), DX
-    ADDQ $4, SI
-    SUBQ $4, CX
-    CMPQ DX, CX
-    JA errTooShort
-    CMPQ DX, $0x7FFFFFFF
+    ADDQ AX, BX // end
+loop:
+    MOVL (AX), CX
+
+    CMPL CX, $0x7FFFFFFF
     JA errTooLarge
 
-    ADDQ DX, SI
-    CMPQ SI, BX
-    JNE loop
+    LEAQ 4(AX)(CX*1), AX
+    CMPQ AX, BX
+    JA errTooShort
+    JB loop
 done:
-    MOVQ $0, CX
-    MOVQ CX, ret+24(FP)
+    XORQ AX, AX
+return:
+    MOVQ AX, ret+24(FP)
     RET
 errTooShort:
-    MOVQ $1, CX
-    MOVQ CX, ret+24(FP)
-    RET
+    MOVQ $1, AX
+    JMP return
 errTooLarge:
-    MOVQ $2, CX
-    MOVQ CX, ret+24(FP)
-    RET
+    MOVQ $2, AX
+    JMP return
diff --git a/encoding/plain/plain_purego.go b/encoding/plain/plain_purego.go
index 4b4e7ad..5f9a50d 100644
--- a/encoding/plain/plain_purego.go
+++ b/encoding/plain/plain_purego.go
@@ -2,7 +2,7 @@
 
 package plain
 
-func validateByteArrays(b []byte) status {
+func validateByteArray(b []byte) status {
 	for i := 0; i < len(b); {
 		r := len(b) - i
 		if r < ByteArrayLengthSize {
diff --git a/encoding/rle/rle.go b/encoding/rle/rle.go
index 9fba3df..1aa03c3 100644
--- a/encoding/rle/rle.go
+++ b/encoding/rle/rle.go
@@ -341,7 +341,7 @@ func decodeInt32(dst, src []byte, bitWidth uint) ([]byte, error) {
 		return dst, errDecodeInvalidBitWidth("INT32", bitWidth)
 	}
 
-	buf := make([]byte, 2*bitpack.Padding)
+	buf := make([]byte, 2*bitpack.PaddingInt32)
 
 	for i := 0; i < len(src); {
 		u, n := binary.Uvarint(src[i:])
@@ -367,10 +367,10 @@ func decodeInt32(dst, src []byte, bitWidth uint) ([]byte, error) {
 			// buffer we can use it, otherwise we have to copy it to a larger
 			// location (which should rarely happen).
 			in := src[i : i+length]
-			if (cap(in) - len(in)) >= bitpack.Padding {
+			if (cap(in) - len(in)) >= bitpack.PaddingInt32 {
 				in = in[:cap(in)]
 			} else {
-				buf = resize(buf, len(in)+bitpack.Padding)
+				buf = resize(buf, len(in)+bitpack.PaddingInt32)
 				copy(buf, in)
 				in = buf
 			}
diff --git a/internal/bitpack/unpack.go b/internal/bitpack/unpack.go
index 0e948ea..752a6f7 100644
--- a/internal/bitpack/unpack.go
+++ b/internal/bitpack/unpack.go
@@ -1,15 +1,19 @@
 package bitpack
 
-// Padding is the padding expected to exist after the end of input buffers for
-// the unpacking algorithms to avoid reading beyond the end of the input.
-const Padding = 16
+// PaddingInt32 is the padding expected to exist after the end of input buffers
+// for the UnpackInt32 algorithm to avoid reading beyond the end of the input.
+const PaddingInt32 = 16
+
+// PaddingInt64 is the padding expected to exist after the end of input buffers
+// for the UnpackInt32 algorithm to avoid reading beyond the end of the input.
+const PaddingInt64 = 32
 
 // UnpackInt32 unpacks 32 bit integers from src to dst.
 //
 // The function unpacked len(dst) integers, it panics if src is too short to
 // contain len(dst) values of the given bit width.
 func UnpackInt32(dst []int32, src []byte, bitWidth uint) {
-	assertUnpack(src, len(dst), bitWidth)
+	_ = src[:ByteCount(bitWidth*uint(len(dst))+8*PaddingInt32)]
 	unpackInt32(dst, src, bitWidth)
 }
 
@@ -18,10 +22,6 @@ func UnpackInt32(dst []int32, src []byte, bitWidth uint) {
 // The function unpacked len(dst) integers, it panics if src is too short to
 // contain len(dst) values of the given bit width.
 func UnpackInt64(dst []int64, src []byte, bitWidth uint) {
-	assertUnpack(src, len(dst), bitWidth)
+	_ = src[:ByteCount(bitWidth*uint(len(dst))+8*PaddingInt64)]
 	unpackInt64(dst, src, bitWidth)
 }
-
-func assertUnpack(src []byte, count int, bitWidth uint) {
-	_ = src[:ByteCount(bitWidth*uint(count)+8*Padding)]
-}
diff --git a/internal/bitpack/unpack_int32_amd64.s b/internal/bitpack/unpack_int32_amd64.s
index 94f5d86..e0f2bf4 100644
--- a/internal/bitpack/unpack_int32_amd64.s
+++ b/internal/bitpack/unpack_int32_amd64.s
@@ -3,7 +3,7 @@
 #include "funcdata.h"
 #include "textflag.h"
 
-// func unpackInt32Default(dst []int32, src []uint32, bitWidth uint)
+// func unpackInt32Default(dst []int32, src []byte, bitWidth uint)
 TEXT ·unpackInt32Default(SB), NOSPLIT, $0-56
     MOVQ dst_base+0(FP), AX
     MOVQ dst_len+8(FP), DX
@@ -95,7 +95,7 @@ test:
 // In this version of the algorithm, we can perform a single memory load in each
 // loop iteration since we know that 8 values will fit in a single XMM register.
 //
-// func unpackInt32x1to16bitsAVX2(dst []int32, src []uint32, bitWidth uint)
+// func unpackInt32x1to16bitsAVX2(dst []int32, src []byte, bitWidth uint)
 TEXT ·unpackInt32x1to16bitsAVX2(SB), NOSPLIT, $56-56
     NO_LOCAL_POINTERS
     MOVQ dst_base+0(FP), AX
@@ -115,7 +115,7 @@ TEXT ·unpackInt32x1to16bitsAVX2(SB), NOSPLIT, $56-56
     SHLQ CX, R8
     DECQ R8
     MOVQ R8, X0
-    VPBROADCASTD X0, X0
+    VPBROADCASTD X0, X0 // bitMask = (1 << bitWidth) - 1
 
     MOVQ CX, R9
     DECQ R9
@@ -173,7 +173,7 @@ done:
 // In this version of the algorithm, we need to 32 bytes at each loop iteration
 // because 8 bit-packed values will span across two XMM registers.
 //
-// func unpackInt32x17to26bitsAVX2(dst []int32, src []uint32, bitWidth uint)
+// func unpackInt32x17to26bitsAVX2(dst []int32, src []byte, bitWidth uint)
 TEXT ·unpackInt32x17to26bitsAVX2(SB), NOSPLIT, $56-56
     NO_LOCAL_POINTERS
     MOVQ dst_base+0(FP), AX
@@ -262,7 +262,7 @@ done:
 //
 // The amount of LEFT shifts is always "8 minus the amount of RIGHT shift".
 //
-// func unpackInt32x27to31bitsAVX2(dst []int32, src []uint32, bitWidth uint)
+// func unpackInt32x27to31bitsAVX2(dst []int32, src []byte, bitWidth uint)
 TEXT ·unpackInt32x27to31bitsAVX2(SB), NOSPLIT, $56-56
     NO_LOCAL_POINTERS
     MOVQ dst_base+0(FP), AX
diff --git a/internal/bitpack/unpack_int64_amd64.go b/internal/bitpack/unpack_int64_amd64.go
index ac4b38a..011c0ca 100644
--- a/internal/bitpack/unpack_int64_amd64.go
+++ b/internal/bitpack/unpack_int64_amd64.go
@@ -2,13 +2,22 @@
 
 package bitpack
 
-import "github.com/segmentio/parquet-go/internal/unsafecast"
+import (
+	"github.com/segmentio/parquet-go/internal/unsafecast"
+	"golang.org/x/sys/cpu"
+)
 
 //go:noescape
 func unpackInt64Default(dst []int64, src []byte, bitWidth uint)
 
+//go:noescape
+func unpackInt64x1to32bitsAVX2(dst []int64, src []byte, bitWidth uint)
+
 func unpackInt64(dst []int64, src []byte, bitWidth uint) {
+	hasAVX2 := cpu.X86.HasAVX2
 	switch {
+	case hasAVX2 && bitWidth <= 32:
+		unpackInt64x1to32bitsAVX2(dst, src, bitWidth)
 	case bitWidth == 64:
 		copy(dst, unsafecast.BytesToInt64(src))
 	default:
diff --git a/internal/bitpack/unpack_int64_amd64.s b/internal/bitpack/unpack_int64_amd64.s
index 122ca27..91d28f6 100644
--- a/internal/bitpack/unpack_int64_amd64.s
+++ b/internal/bitpack/unpack_int64_amd64.s
@@ -67,3 +67,132 @@ test:
     CMPQ SI, DX
     JNE loop
     RET
+
+// This bit unpacking function was inspired from the 32 bit version, but
+// adapted to account for the fact that eight 64 bit values span across
+// two YMM registers, and across lanes of YMM registers.
+//
+// Because of the two lanes of YMM registers, we cannot use the VPSHUFB
+// instruction to dispatch bytes of the input to the registers. Instead we use
+// the VPERMD instruction, which has higher latency but supports dispatching
+// bytes across register lanes. Measurable throughput gains remain despite the
+// algorithm running on a few more CPU cycles per loop.
+//
+// The initialization phase of this algorithm generates masks for
+// permutations and shifts used to decode the bit-packed values.
+//
+// The permutation masks are written to Y7 and Y8, and contain the results
+// of this formula:
+//
+//      temp[i] = (bitWidth * i) / 32
+//      mask[i] = temp[i] | ((temp[i] + 1) << 32)
+//
+// Since VPERMQ only supports reading the permutation combination from an
+// immediate value, we use VPERMD and generate permutation for pairs of two
+// consecutive 32 bit words, which is why we have the upper part of each 64
+// bit word set with (x+1)<<32.
+//
+// The masks for right shifts are written to Y5 and Y6, and computed with
+// this formula:
+//
+//      shift[i] = (bitWidth * i) - (32 * ((bitWidth * i) / 32))
+//
+// The amount to shift by is the number of values previously unpacked, offseted
+// by the byte count of 32 bit words that we read from first bits from.
+//
+// Technically the masks could be precomputed and declared in global tables;
+// however, declaring masks for all bit width is tedious and makes code
+// maintenance more costly for no measurable benefits on production workloads.
+//
+// func unpackInt64x1to32bitsAVX2(dst []int64, src []byte, bitWidth uint)
+TEXT ·unpackInt64x1to32bitsAVX2(SB), NOSPLIT, $56-56
+    NO_LOCAL_POINTERS
+    MOVQ dst_base+0(FP), AX
+    MOVQ dst_len+8(FP), DX
+    MOVQ src_base+24(FP), BX
+    MOVQ bitWidth+48(FP), CX
+
+    CMPQ DX, $8
+    JB tail
+
+    MOVQ DX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+    XORQ SI, SI
+
+    MOVQ $1, R8
+    SHLQ CX, R8
+    DECQ R8
+    MOVQ R8, X0
+    VPBROADCASTQ X0, Y0 // bitMask = (1 << bitWidth) - 1
+
+    VPCMPEQQ Y1, Y1, Y1
+    VPSRLQ $63, Y1, Y1  // [1,1,1,1]
+
+    MOVQ CX, X2
+    VPBROADCASTQ X2, Y2 // [bitWidth]
+
+    VMOVDQU range0n7<>+0(SB), Y3  // [0,1,2,3]
+    VMOVDQU range0n7<>+32(SB), Y4 // [4,5,6,7]
+
+    VPMULLD Y2, Y3, Y5 // [bitWidth] * [0,1,2,3]
+    VPMULLD Y2, Y4, Y6 // [bitWidth] * [4,5,6,7]
+
+    VPSRLQ $5, Y5, Y7 // ([bitWidth] * [0,1,2,3]) / 32
+    VPSRLQ $5, Y6, Y8 // ([bitWidth] * [4,5,6,7]) / 32
+
+    VPSLLQ $5, Y7, Y9  // (([bitWidth] * [0,1,2,3]) / 32) * 32
+    VPSLLQ $5, Y8, Y10 // (([bitWidth] * [4,5,6,7]) / 32) * 32
+
+    VPADDQ Y1, Y7, Y11
+    VPADDQ Y1, Y8, Y12
+    VPSLLQ $32, Y11, Y11
+    VPSLLQ $32, Y12, Y12
+    VPOR Y11, Y7, Y7 // permutations[i] = [i | ((i + 1) << 32)]
+    VPOR Y12, Y8, Y8 // permutations[i] = [i | ((i + 1) << 32)]
+
+    VPSUBQ Y9, Y5, Y5 // shifts
+    VPSUBQ Y10, Y6, Y6
+loop:
+    VMOVDQU (BX), Y1
+
+    VPERMD Y1, Y7, Y2
+    VPERMD Y1, Y8, Y3
+
+    VPSRLVQ Y5, Y2, Y2
+    VPSRLVQ Y6, Y3, Y3
+
+    VPAND Y0, Y2, Y2
+    VPAND Y0, Y3, Y3
+
+    VMOVDQU Y2, (AX)(SI*8)
+    VMOVDQU Y3, 32(AX)(SI*8)
+
+    ADDQ CX, BX
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loop
+    VZEROUPPER
+
+    CMPQ SI, DX
+    JE done
+    LEAQ (AX)(SI*8), AX
+    SUBQ SI, DX
+tail:
+    MOVQ AX, dst_base-56(SP)
+    MOVQ DX, dst_len-48(SP)
+    MOVQ BX, src_base-32(SP)
+    MOVQ CX, bitWidth-8(SP)
+    CALL ·unpackInt64Default(SB)
+done:
+    RET
+
+GLOBL range0n7<>(SB), RODATA|NOPTR, $64
+DATA range0n7<>+0(SB)/8,  $0
+DATA range0n7<>+8(SB)/8,  $1
+DATA range0n7<>+16(SB)/8, $2
+DATA range0n7<>+24(SB)/8, $3
+DATA range0n7<>+32(SB)/8, $4
+DATA range0n7<>+40(SB)/8, $5
+DATA range0n7<>+48(SB)/8, $6
+DATA range0n7<>+56(SB)/8, $7
diff --git a/internal/quick/quick.go b/internal/quick/quick.go
index bce7ef0..2bb51f2 100644
--- a/internal/quick/quick.go
+++ b/internal/quick/quick.go
@@ -2,6 +2,7 @@ package quick
 
 import (
 	"fmt"
+	"math"
 	"math/rand"
 	"reflect"
 	"strings"
@@ -77,7 +78,7 @@ func MakeValueFuncOf(t reflect.Type) MakeValueFunc {
 
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 		return func(v reflect.Value, r *rand.Rand) {
-			v.SetInt(r.Int63())
+			v.SetInt(r.Int63n(math.MaxInt32))
 		}
 
 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:


Test patch:
diff --git a/encoding/delta/length_byte_array_test.go b/encoding/delta/length_byte_array_test.go
index c7c14b6..32f7755 100644
--- a/encoding/delta/length_byte_array_test.go
+++ b/encoding/delta/length_byte_array_test.go
@@ -28,7 +28,7 @@ func TestDecodeLengthByteArray(t *testing.T) {
 	}
 
 	index := 0
-	err = plain.RangeByteArrays(dst, func(got []byte) error {
+	err = plain.RangeByteArray(dst, func(got []byte) error {
 		want := characters[:index%len(characters)]
 
 		if want != string(got) {
diff --git a/encoding/plain/plain_test.go b/encoding/plain/plain_test.go
index fa610dd..84eb7b3 100644
--- a/encoding/plain/plain_test.go
+++ b/encoding/plain/plain_test.go
@@ -33,14 +33,14 @@ func TestAppendBoolean(t *testing.T) {
 	}
 }
 
-func TestValidateByteArrays(t *testing.T) {
+func TestValidateByteArray(t *testing.T) {
 	t.Run("ok", func(t *testing.T) {
 		var b []byte
 		b = plain.AppendByteArrayString(b, "Hello")
 		b = plain.AppendByteArrayString(b, "World")
 		b = plain.AppendByteArrayString(b, "!")
 
-		if err := plain.ValidateByteArrays(b); err != nil {
+		if err := plain.ValidateByteArray(b); err != nil {
 			t.Error(err)
 		}
 	})
@@ -51,7 +51,7 @@ func TestValidateByteArrays(t *testing.T) {
 		b = plain.AppendByteArrayString(b, "World")
 		b = plain.AppendByteArrayString(b, "!")
 
-		if plain.ValidateByteArrays(b[:len(b)-1]) == nil {
+		if plain.ValidateByteArray(b[:len(b)-1]) == nil {
 			t.Error("expected non-nil error")
 		}
 	})
diff --git a/internal/bitpack/unpack_test.go b/internal/bitpack/unpack_test.go
index b906aec..e77bab6 100644
--- a/internal/bitpack/unpack_test.go
+++ b/internal/bitpack/unpack_test.go
@@ -25,7 +25,7 @@ func TestUnpackInt32(t *testing.T) {
 			}
 
 			size := (blockSize * bitWidth) / 8
-			buf := make([]byte, size+bitpack.Padding)
+			buf := make([]byte, size+bitpack.PaddingInt32)
 			bitpack.PackInt32(buf, block[:], bitWidth)
 
 			src := buf[:size]
@@ -39,7 +39,7 @@ func TestUnpackInt32(t *testing.T) {
 				bitpack.UnpackInt32(dst[:n], src, bitWidth)
 
 				if !reflect.DeepEqual(block[:n], dst[:n]) {
-					t.Errorf("values mismatch for length=%d\nwant: %v\ngot:  %v", n, block[:n], dst[:n])
+					t.Fatalf("values mismatch for length=%d\nwant: %v\ngot:  %v", n, block[:n], dst[:n])
 				}
 			}
 		})
@@ -58,7 +58,7 @@ func TestUnpackInt64(t *testing.T) {
 			}
 
 			size := (blockSize * bitWidth) / 8
-			buf := make([]byte, size+bitpack.Padding)
+			buf := make([]byte, size+bitpack.PaddingInt64)
 			bitpack.PackInt64(buf, block[:], bitWidth)
 
 			src := buf[:size]
@@ -72,7 +72,7 @@ func TestUnpackInt64(t *testing.T) {
 				bitpack.UnpackInt64(dst[:n], src, bitWidth)
 
 				if !reflect.DeepEqual(block[:n], dst[:n]) {
-					t.Errorf("values mismatch for length=%d\nwant: %v\ngot:  %v", n, block[:n], dst[:n])
+					t.Fatalf("values mismatch for length=%d\nwant: %v\ngot:  %v", n, block[:n], dst[:n])
 				}
 			}
 		})
@@ -82,7 +82,7 @@ func TestUnpackInt64(t *testing.T) {
 func BenchmarkUnpackInt32(b *testing.B) {
 	for bitWidth := uint(1); bitWidth <= 32; bitWidth++ {
 		block := [blockSize]int32{}
-		buf := [4*blockSize + bitpack.Padding]byte{}
+		buf := [4*blockSize + bitpack.PaddingInt32]byte{}
 		bitpack.PackInt32(buf[:], block[:], bitWidth)
 
 		b.Run(fmt.Sprintf("bitWidth=%d", bitWidth), func(b *testing.B) {
@@ -101,7 +101,7 @@ func BenchmarkUnpackInt32(b *testing.B) {
 func BenchmarkUnpackInt64(b *testing.B) {
 	for bitWidth := uint(1); bitWidth <= 64; bitWidth++ {
 		block := [blockSize]int64{}
-		buf := [8*blockSize + bitpack.Padding]byte{}
+		buf := [8*blockSize + bitpack.PaddingInt64]byte{}
 		bitpack.PackInt64(buf[:], block[:], bitWidth)
 
 		b.Run(fmt.Sprintf("bitWidth=%d", bitWidth), func(b *testing.B) {
diff --git a/parquet_test.go b/parquet_test.go
index c0a977b..88615f0 100644
--- a/parquet_test.go
+++ b/parquet_test.go
@@ -58,7 +58,7 @@ func (row int32Column) generate(prng *rand.Rand) int32Column {
 }
 
 type int64Column struct {
-	Value int64
+	Value int64 `parquet:",delta"`
 }
 
 func (row int64Column) generate(prng *rand.Rand) int64Column {