Repository: segmentio/parquet-go
Pull request #203 (2022-06-01 16:55:42)

add parquet.Row.Clone
Following up from user feedback, this PR adds a `Clone` method to the `parquet.Row` type to support common use cases of having to capture the values read from calls to `parquet.RowReader.ReadRows`.



Patch:
diff --git a/column_buffer.go b/column_buffer.go
index a5a3542..a4ff06c 100644
--- a/column_buffer.go
+++ b/column_buffer.go
@@ -792,16 +792,18 @@ func (col *booleanColumnBuffer) writeValues(rows array, size, offset uintptr) {
 		// First we attempt to write enough bits to align the number of values
 		// in the column buffer on 8 bytes. After this step the next bit should
 		// be written at the zero'th index of a byte of the buffer.
-		var b byte
-		for i < r {
-			v := *(*byte)(rows.index(i, size, offset))
-			b |= (v & 1) << uint(i)
-			i++
+		if r < 8 {
+			var b byte
+			for i < r {
+				v := *(*byte)(rows.index(i, size, offset))
+				b |= (v & 1) << uint(i)
+				i++
+			}
+			x := uint(col.numValues) / 8
+			y := uint(col.numValues) % 8
+			col.bits[x] |= (b << y) | (col.bits[x] & ^(0xFF << y))
+			col.numValues += int32(i)
 		}
-		x := uint(col.numValues) / 8
-		y := uint(col.numValues) % 8
-		col.bits[x] |= (b << y) | (col.bits[x] & ^(0xFF << y))
-		col.numValues += int32(i)
 
 		if n := rows.len - i; n >= 8 {
 			// At this stage, we know that that we have at least 8 bits to write
@@ -810,26 +812,13 @@ func (col *booleanColumnBuffer) writeValues(rows array, size, offset uintptr) {
 			// packing them into a single byte and writing it to the output
 			// buffer. This effectively reduces by 87.5% the number of memory
 			// stores that the program needs to perform to generate the values.
-			for j := i + (n/8)*8; i < j; i += 8 {
-				b0 := *(*byte)(rows.index(i+0, size, offset))
-				b1 := *(*byte)(rows.index(i+1, size, offset))
-				b2 := *(*byte)(rows.index(i+2, size, offset))
-				b3 := *(*byte)(rows.index(i+3, size, offset))
-				b4 := *(*byte)(rows.index(i+4, size, offset))
-				b5 := *(*byte)(rows.index(i+5, size, offset))
-				b6 := *(*byte)(rows.index(i+6, size, offset))
-				b7 := *(*byte)(rows.index(i+7, size, offset))
-
-				col.bits[col.numValues/8] = (b0 & 1) |
-					((b1 & 1) << 1) |
-					((b2 & 1) << 2) |
-					((b3 & 1) << 3) |
-					((b4 & 1) << 4) |
-					((b5 & 1) << 5) |
-					((b6 & 1) << 6) |
-					((b7 & 1) << 7)
-				col.numValues += 8
+			section := array{
+				ptr: rows.index(i, size, 0),
+				len: ((rows.len - i) / 8) * 8,
 			}
+			writeValuesBool(col.bits[col.numValues/8:], section, size, offset)
+			col.numValues += int32(section.len)
+			i += section.len
 		}
 	}
 
@@ -939,14 +928,9 @@ func (col *int32ColumnBuffer) writeValues(rows array, size, offset uintptr) {
 	if n := len(col.values) + rows.len; n > cap(col.values) {
 		col.values = append(make([]int32, 0, max(n, 2*cap(col.values))), col.values...)
 	}
-
 	n := len(col.values)
 	col.values = col.values[:n+rows.len]
-
-	values := col.values[n:]
-	for i := range values {
-		values[i] = *(*int32)(rows.index(i, size, offset))
-	}
+	writeValuesInt32(col.values[n:], rows, size, offset)
 }
 
 func (col *int32ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -1043,14 +1027,9 @@ func (col *int64ColumnBuffer) writeValues(rows array, size, offset uintptr) {
 	if n := len(col.values) + rows.len; n > cap(col.values) {
 		col.values = append(make([]int64, 0, max(n, 2*cap(col.values))), col.values...)
 	}
-
 	n := len(col.values)
 	col.values = col.values[:n+rows.len]
-
-	values := col.values[n:]
-	for i := range values {
-		values[i] = *(*int64)(rows.index(i, size, offset))
-	}
+	writeValuesInt64(col.values[n:], rows, size, offset)
 }
 
 func (col *int64ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -1241,14 +1220,9 @@ func (col *floatColumnBuffer) writeValues(rows array, size, offset uintptr) {
 	if n := len(col.values) + rows.len; n > cap(col.values) {
 		col.values = append(make([]float32, 0, max(n, 2*cap(col.values))), col.values...)
 	}
-
 	n := len(col.values)
 	col.values = col.values[:n+rows.len]
-
-	values := col.values[n:]
-	for i := range values {
-		values[i] = *(*float32)(rows.index(i, size, offset))
-	}
+	writeValuesFloat32(col.values[n:], rows, size, offset)
 }
 
 func (col *floatColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -1345,14 +1319,9 @@ func (col *doubleColumnBuffer) writeValues(rows array, size, offset uintptr) {
 	if n := len(col.values) + rows.len; n > cap(col.values) {
 		col.values = append(make([]float64, 0, max(n, 2*cap(col.values))), col.values...)
 	}
-
 	n := len(col.values)
 	col.values = col.values[:n+rows.len]
-
-	values := col.values[n:]
-	for i := range values {
-		values[i] = *(*float64)(rows.index(i, size, offset))
-	}
+	writeValuesFloat64(col.values[n:], rows, size, offset)
 }
 
 func (col *doubleColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -1627,21 +1596,15 @@ func (col *fixedLenByteArrayColumnBuffer) writeValues(rows array, size, offset u
 	}
 }
 
-func (col *fixedLenByteArrayColumnBuffer) writeValues128(rows array, size, offset uintptr) {
+func (col *fixedLenByteArrayColumnBuffer) writeValuesUint128(rows array, size, offset uintptr) {
 	c := cap(col.data)
 	n := len(col.data) + (16 * rows.len)
 	if c < n {
 		col.data = append(make([]byte, 0, max(n, 2*c)), col.data...)
 	}
-
 	firstIndex := len(col.data)
 	col.data = col.data[:len(col.data)+(16*rows.len)]
-
-	data := unsafe.Pointer(&col.data[firstIndex])
-	for i := 0; i < rows.len; i++ {
-		p := rows.index(i, size, offset)
-		*(*[16]byte)(unsafe.Add(data, i*16)) = *(*[16]byte)(p)
-	}
+	writeValuesUint128(col.data[firstIndex:], rows, size, offset)
 }
 
 func (col *fixedLenByteArrayColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -1738,14 +1701,9 @@ func (col *uint32ColumnBuffer) writeValues(rows array, size, offset uintptr) {
 	if n := len(col.values) + rows.len; n > cap(col.values) {
 		col.values = append(make([]uint32, 0, max(n, 2*cap(col.values))), col.values...)
 	}
-
 	n := len(col.values)
 	col.values = col.values[:n+rows.len]
-
-	values := col.values[n:]
-	for i := range values {
-		values[i] = *(*uint32)(rows.index(i, size, offset))
-	}
+	writeValuesUint32(col.values[n:], rows, size, offset)
 }
 
 func (col *uint32ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -1842,14 +1800,9 @@ func (col *uint64ColumnBuffer) writeValues(rows array, size, offset uintptr) {
 	if n := len(col.values) + rows.len; n > cap(col.values) {
 		col.values = append(make([]uint64, 0, max(n, 2*cap(col.values))), col.values...)
 	}
-
 	n := len(col.values)
 	col.values = col.values[:n+rows.len]
-
-	values := col.values[n:]
-	for i := range values {
-		values[i] = *(*uint64)(rows.index(i, size, offset))
-	}
+	writeValuesUint64(col.values[n:], rows, size, offset)
 }
 
 func (col *uint64ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
diff --git a/column_buffer_amd64.go b/column_buffer_amd64.go
new file mode 100644
index 0000000..976791d
--- /dev/null
+++ b/column_buffer_amd64.go
@@ -0,0 +1,55 @@
+//go:build !purego
+
+package parquet
+
+import (
+	"unsafe"
+
+	"golang.org/x/sys/cpu"
+)
+
+var hasAVX2 = cpu.X86.HasAVX2
+
+//go:noescape
+func writeValuesBitpack(values unsafe.Pointer, rows array, size, offset uintptr)
+
+//go:noescape
+func writeValues32bits(values unsafe.Pointer, rows array, size, offset uintptr)
+
+//go:noescpae
+func writeValues64bits(values unsafe.Pointer, rows array, size, offset uintptr)
+
+//go:noescape
+func writeValues128bits(values unsafe.Pointer, rows array, size, offset uintptr)
+
+func writeValuesBool(values []byte, rows array, size, offset uintptr) {
+	writeValuesBitpack(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesInt32(values []int32, rows array, size, offset uintptr) {
+	writeValues32bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesInt64(values []int64, rows array, size, offset uintptr) {
+	writeValues64bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesUint32(values []uint32, rows array, size, offset uintptr) {
+	writeValues32bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesUint64(values []uint64, rows array, size, offset uintptr) {
+	writeValues64bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesUint128(values []byte, rows array, size, offset uintptr) {
+	writeValues128bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesFloat32(values []float32, rows array, size, offset uintptr) {
+	writeValues32bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesFloat64(values []float64, rows array, size, offset uintptr) {
+	writeValues64bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
diff --git a/column_buffer_amd64.s b/column_buffer_amd64.s
new file mode 100644
index 0000000..dbcdd98
--- /dev/null
+++ b/column_buffer_amd64.s
@@ -0,0 +1,268 @@
+//go:build !purego
+
+#include "textflag.h"
+
+// func writeValuesBitpack(values unsafe.Pointer, rows array, size, offset uintptr)
+TEXT ·writeValuesBitpack(SB), NOSPLIT, $0-40
+    MOVQ values_base+0(FP), AX
+    MOVQ rows_base+8(FP), BX
+    MOVQ rows_len+16(FP), CX
+    MOVQ size+24(FP), DX
+    MOVQ offset+32(FP), DI
+
+    CMPQ CX, $0
+    JNE init
+    RET
+init:
+    ADDQ DI, BX
+    SHRQ $3, CX
+    XORQ SI, SI
+
+    CMPB ·hasAVX2(SB), $0
+    JE loop
+
+    // Make sure `size - offset` is at least 4 bytes, otherwise VPGATHERDD
+    // may read data beyond the end of the program memory and trigger a fault.
+    //
+    // If the boolean values do not have enough padding we must fallback to the
+    // scalar algorithm to be able to load single bytes from memory.
+    MOVQ DX, R8
+    SUBQ DI, R8
+    CMPQ R8, $4
+    JB loop
+
+    VPBROADCASTD size+24(FP), Y0
+    VPMULLD scale8x4<>(SB), Y0, Y0
+    VPCMPEQD Y1, Y1, Y1
+    VPCMPEQD Y2, Y2, Y2
+    VPCMPEQD Y3, Y3, Y3
+    VPSRLD $31, Y3, Y3
+avx2loop:
+    VPGATHERDD Y1, (BX)(Y0*1), Y4
+    VMOVDQU Y2, Y1
+    VPAND Y3, Y4, Y4
+    VPSLLD $31, Y4, Y4
+    VMOVMSKPS Y4, DI
+
+    MOVB DI, (AX)(SI*1)
+
+    LEAQ (BX)(DX*8), BX
+    INCQ SI
+    CMPQ SI, CX
+    JNE avx2loop
+    VZEROUPPER
+    RET
+loop:
+    LEAQ (BX)(DX*2), DI
+    MOVBQZX (BX), R8
+    MOVBQZX (BX)(DX*1), R9
+    MOVBQZX (DI), R10
+    MOVBQZX (DI)(DX*1), R11
+    LEAQ (BX)(DX*4), BX
+    LEAQ (DI)(DX*4), DI
+    MOVBQZX (BX), R12
+    MOVBQZX (BX)(DX*1), R13
+    MOVBQZX (DI), R14
+    MOVBQZX (DI)(DX*1), R15
+    LEAQ (BX)(DX*4), BX
+
+    ANDQ $1, R8
+    ANDQ $1, R9
+    ANDQ $1, R10
+    ANDQ $1, R11
+    ANDQ $1, R12
+    ANDQ $1, R13
+    ANDQ $1, R14
+    ANDQ $1, R15
+
+    SHLQ $1, R9
+    SHLQ $2, R10
+    SHLQ $3, R11
+    SHLQ $4, R12
+    SHLQ $5, R13
+    SHLQ $6, R14
+    SHLQ $7, R15
+
+    ORQ R9, R8
+    ORQ R11, R10
+    ORQ R13, R12
+    ORQ R15, R14
+    ORQ R10, R8
+    ORQ R12, R8
+    ORQ R14, R8
+
+    MOVB R8, (AX)(SI*1)
+
+    INCQ SI
+    CMPQ SI, CX
+    JNE loop
+    RET
+
+// func writeValues32bits(values unsafe.Pointer, rows array, size, offset uintptr)
+TEXT ·writeValues32bits(SB), NOSPLIT, $0-40
+    MOVQ values_base+0(FP), AX
+    MOVQ rows_base+8(FP), BX
+    MOVQ rows_len+16(FP), CX
+    MOVQ size+24(FP), DX
+
+    XORQ SI, SI
+    ADDQ offset+32(FP), BX
+
+    CMPQ CX, $0
+    JE done
+
+    CMPQ CX, $8
+    JB loop1x4
+
+    CMPB ·hasAVX2(SB), $0
+    JE loop1x4
+
+    MOVQ CX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+
+    VPBROADCASTD size+24(FP), Y0
+    VPMULLD scale8x4<>(SB), Y0, Y0
+    VPCMPEQD Y1, Y1, Y1
+    VPCMPEQD Y2, Y2, Y2
+loop8x4:
+    VPGATHERDD Y1, (BX)(Y0*1), Y3
+    VMOVDQU Y3, (AX)(SI*4)
+    VMOVDQU Y2, Y1
+
+    LEAQ (BX)(DX*8), BX
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loop8x4
+    VZEROUPPER
+
+    CMPQ SI, CX
+    JE done
+
+loop1x4:
+    MOVL (BX), R8
+    MOVL R8, (AX)(SI*4)
+
+    ADDQ DX, BX
+    INCQ SI
+    CMPQ SI, CX
+    JNE loop1x4
+done:
+    RET
+
+// func writeValues64bits(values unsafe.Pointer, rows array, size, offset uintptr)
+TEXT ·writeValues64bits(SB), NOSPLIT, $0-40
+    MOVQ values_base+0(FP), AX
+    MOVQ rows_base+8(FP), BX
+    MOVQ rows_len+16(FP), CX
+    MOVQ size+24(FP), DX
+
+    XORQ SI, SI
+    ADDQ offset+32(FP), BX
+
+    CMPQ CX, $0
+    JE done
+
+    CMPQ CX, $4
+    JB loop1x8
+
+    CMPB ·hasAVX2(SB), $0
+    JE loop1x8
+
+    MOVQ CX, DI
+    SHRQ $2, DI
+    SHLQ $2, DI
+
+    VPBROADCASTQ size+24(FP), Y0
+    VPMULLD scale4x8<>(SB), Y0, Y0
+    VPCMPEQD Y1, Y1, Y1
+    VPCMPEQD Y2, Y2, Y2
+loop4x8:
+    VPGATHERQQ Y1, (BX)(Y0*1), Y3
+    VMOVDQU Y3, (AX)(SI*8)
+    VMOVDQU Y2, Y1
+
+    LEAQ (BX)(DX*4), BX
+    ADDQ $4, SI
+    CMPQ SI, DI
+    JNE loop4x8
+    VZEROUPPER
+
+    CMPQ SI, CX
+    JE done
+loop1x8:
+    MOVQ (BX), R8
+    MOVQ R8, (AX)(SI*8)
+
+    ADDQ DX, BX
+    INCQ SI
+    CMPQ SI, CX
+    JNE loop1x8
+done:
+    RET
+
+// func writeValues128bits(values unsafe.Pointer, rows array, size, offset uintptr)
+TEXT ·writeValues128bits(SB), NOSPLIT, $0-40
+    MOVQ values_base+0(FP), AX
+    MOVQ rows_base+8(FP), BX
+    MOVQ rows_len+16(FP), CX
+    MOVQ size+24(FP), DX
+
+    XORQ SI, SI
+    ADDQ offset+32(FP), BX
+
+    CMPQ CX, $0
+    JE done
+
+    CMPQ CX, $2
+    JB loop1x16
+
+    CMPB ·hasAVX2(SB), $0
+    JE loop1x16
+
+    MOVQ CX, DI
+    SHRQ $1, DI
+    SHLQ $1, DI
+loop2x16:
+    VMOVDQU (BX), X0
+    VMOVDQU (BX)(DX*1), X1
+
+    VMOVDQU X0, (AX)
+    VMOVDQU X1, 16(AX)
+
+    ADDQ $32, AX
+    LEAQ (BX)(DX*2), BX
+    ADDQ $2, SI
+    CMPQ SI, DI
+    JNE loop2x16
+    VZEROUPPER
+
+    CMPQ SI, CX
+    JE done
+loop1x16:
+    MOVOU (BX), X0
+    MOVOU X0, (AX)
+
+    ADDQ $16, AX
+    ADDQ DX, BX
+    INCQ SI
+    CMPQ SI, CX
+    JNE loop1x16
+done:
+    RET
+
+GLOBL scale8x4<>(SB), RODATA|NOPTR, $32
+DATA scale8x4<>+0(SB)/4,  $0
+DATA scale8x4<>+4(SB)/4,  $1
+DATA scale8x4<>+8(SB)/4,  $2
+DATA scale8x4<>+12(SB)/4, $3
+DATA scale8x4<>+16(SB)/4, $4
+DATA scale8x4<>+20(SB)/4, $5
+DATA scale8x4<>+24(SB)/4, $6
+DATA scale8x4<>+28(SB)/4, $7
+
+GLOBL scale4x8<>(SB), RODATA|NOPTR, $32
+DATA scale4x8<>+0(SB)/8,  $0
+DATA scale4x8<>+8(SB)/8,  $1
+DATA scale4x8<>+16(SB)/8, $2
+DATA scale4x8<>+24(SB)/8, $3
diff --git a/column_buffer_go18.go b/column_buffer_go18.go
index 6cbe9f8..10523a0 100644
--- a/column_buffer_go18.go
+++ b/column_buffer_go18.go
@@ -850,7 +850,7 @@ func (w *columnBufferWriter) writeRowsUUID(rows array, size, offset uintptr, lev
 
 	switch c := w.columns[levels.columnIndex].(type) {
 	case *fixedLenByteArrayColumnBuffer:
-		c.writeValues128(rows, size, offset)
+		c.writeValuesUint128(rows, size, offset)
 	default:
 		w.reset()
 		for i := 0; i < rows.len; i++ {
diff --git a/column_buffer_purego.go b/column_buffer_purego.go
new file mode 100644
index 0000000..dd4303c
--- /dev/null
+++ b/column_buffer_purego.go
@@ -0,0 +1,72 @@
+//go:build !amd64 || purego
+
+package parquet
+
+import "unsafe"
+
+func writeValuesBool(values []byte, rows array, size, offset uintptr) {
+	for i, j := 0, 0; i < rows.len; i += 8 {
+		b0 := *(*byte)(rows.index(i+0, size, offset))
+		b1 := *(*byte)(rows.index(i+1, size, offset))
+		b2 := *(*byte)(rows.index(i+2, size, offset))
+		b3 := *(*byte)(rows.index(i+3, size, offset))
+		b4 := *(*byte)(rows.index(i+4, size, offset))
+		b5 := *(*byte)(rows.index(i+5, size, offset))
+		b6 := *(*byte)(rows.index(i+6, size, offset))
+		b7 := *(*byte)(rows.index(i+7, size, offset))
+
+		values[j] = (b0 & 1) |
+			((b1 & 1) << 1) |
+			((b2 & 1) << 2) |
+			((b3 & 1) << 3) |
+			((b4 & 1) << 4) |
+			((b5 & 1) << 5) |
+			((b6 & 1) << 6) |
+			((b7 & 1) << 7)
+		j++
+	}
+}
+
+func writeValuesInt32(values []int32, rows array, size, offset uintptr) {
+	for i := range values {
+		values[i] = *(*int32)(rows.index(i, size, offset))
+	}
+}
+
+func writeValuesInt64(values []int64, rows array, size, offset uintptr) {
+	for i := range values {
+		values[i] = *(*int64)(rows.index(i, size, offset))
+	}
+}
+
+func writeValuesUint32(values []uint32, rows array, size, offset uintptr) {
+	for i := range values {
+		values[i] = *(*uint32)(rows.index(i, size, offset))
+	}
+}
+
+func writeValuesUint64(values []uint64, rows array, size, offset uintptr) {
+	for i := range values {
+		values[i] = *(*uint64)(rows.index(i, size, offset))
+	}
+}
+
+func writeValuesUint128(values []byte, rows array, size, offset uintptr) {
+	data := unsafe.Pointer(&values[0])
+	for i := 0; i < rows.len; i++ {
+		p := rows.index(i, size, offset)
+		*(*[16]byte)(unsafe.Add(data, i*16)) = *(*[16]byte)(p)
+	}
+}
+
+func writeValuesFloat32(values []float32, rows array, size, offset uintptr) {
+	for i := range values {
+		values[i] = *(*float32)(rows.index(i, size, offset))
+	}
+}
+
+func writeValuesFloat64(values []float64, rows array, size, offset uintptr) {
+	for i := range values {
+		values[i] = *(*float64)(rows.index(i, size, offset))
+	}
+}
diff --git a/config.go b/config.go
index 9546dbd..079ec9a 100644
--- a/config.go
+++ b/config.go
@@ -136,7 +136,6 @@ type WriterConfig struct {
 	CreatedBy            string
 	ColumnPageBuffers    PageBufferPool
 	ColumnIndexSizeLimit int
-	PageBufferPool       PageBufferPool
 	PageBufferSize       int
 	WriteBufferSize      int
 	DataPageVersion      int
diff --git a/encoding/delta/binary_packed.go b/encoding/delta/binary_packed.go
index 6a7c7a2..8389b1a 100644
--- a/encoding/delta/binary_packed.go
+++ b/encoding/delta/binary_packed.go
@@ -9,6 +9,8 @@ import (
 	"github.com/segmentio/parquet-go/encoding"
 	"github.com/segmentio/parquet-go/format"
 	"github.com/segmentio/parquet-go/internal/bits"
+
+	. "math/bits"
 )
 
 const (
@@ -20,120 +22,261 @@ const (
 	//
 	// 65K+ values should be enough for any valid use case.
 	maxSupportedBlockSize = 65536
-)
 
-type BinaryPackedEncoding struct {
-	encoding.NotSupported
-}
+	maxHeaderLength32    = 4 * binary.MaxVarintLen64
+	maxMiniBlockLength32 = binary.MaxVarintLen64 + numMiniBlocks + (4 * blockSize)
 
-func (e *BinaryPackedEncoding) String() string {
-	return "DELTA_BINARY_PACKED"
-}
+	maxHeaderLength64    = 8 * binary.MaxVarintLen64
+	maxMiniBlockLength64 = binary.MaxVarintLen64 + numMiniBlocks + (8 * blockSize)
+)
 
-func (e *BinaryPackedEncoding) Encoding() format.Encoding {
-	return format.DeltaBinaryPacked
-}
+var (
+	encodeInt32 = encodeInt32Default
+	encodeInt64 = encodeInt64Default
+)
 
-func (e *BinaryPackedEncoding) EncodeInt32(dst, src []byte) ([]byte, error) {
-	if (len(src) % 4) != 0 {
-		return dst[:0], encoding.ErrEncodeInvalidInputSize(e, "INT64", len(src))
+func encodeInt32Default(dst []byte, src []int32) []byte {
+	totalValues := len(src)
+	firstValue := int32(0)
+	if totalValues > 0 {
+		firstValue = src[0]
 	}
-	return e.encodeInt32(dst[:0], bits.BytesToInt32(src))
-}
 
-func (e *BinaryPackedEncoding) EncodeInt64(dst, src []byte) ([]byte, error) {
-	if (len(src) % 8) != 0 {
-		return dst[:0], encoding.ErrEncodeInvalidInputSize(e, "INT64", len(src))
+	n := len(dst)
+	dst = resize(dst, n+maxHeaderLength32)
+	dst = dst[:n+encodeBinaryPackedHeader(dst[n:], blockSize, numMiniBlocks, totalValues, int64(firstValue))]
+
+	if totalValues < 2 {
+		return dst
 	}
-	return e.encodeInt64(dst[:0], bits.BytesToInt64(src))
-}
 
-func (e *BinaryPackedEncoding) encodeInt32(dst []byte, src []int32) ([]byte, error) {
-	return e.encode(dst, len(src), func(i int) int64 { return int64(src[i]) })
-}
+	lastValue := firstValue
+	for i := 1; i < len(src); i += blockSize {
+		block := [blockSize]int32{}
+		blockLength := copy(block[:], src[i:])
+
+		lastValue = blockDeltaInt32(&block, lastValue)
+		minDelta := blockMinInt32(&block)
+		blockSubInt32(&block, minDelta)
+		blockClearInt32(&block, blockLength)
+
+		bitWidths := [numMiniBlocks]byte{}
+		blockBitWidthsInt32(&bitWidths, &block)
 
-func (e *BinaryPackedEncoding) encodeInt64(dst []byte, src []int64) ([]byte, error) {
-	return e.encode(dst, len(src), func(i int) int64 { return src[i] })
+		n := len(dst)
+		dst = resize(dst, n+maxMiniBlockLength32+4)
+		n += encodeBlockHeader(dst[n:], int64(minDelta), bitWidths)
+
+		for i, bitWidth := range bitWidths {
+			if bitWidth != 0 {
+				miniBlock := (*[miniBlockSize]int32)(block[i*miniBlockSize:])
+				miniBlockPackInt32(dst[n:], miniBlock, uint(bitWidth))
+				n += (miniBlockSize * int(bitWidth)) / 8
+			}
+		}
+
+		dst = dst[:n]
+	}
+
+	return dst
 }
 
-func (e *BinaryPackedEncoding) encode(dst []byte, totalValues int, valueAt func(int) int64) ([]byte, error) {
+func encodeInt64Default(dst []byte, src []int64) []byte {
+	totalValues := len(src)
 	firstValue := int64(0)
 	if totalValues > 0 {
-		firstValue = valueAt(0)
+		firstValue = src[0]
 	}
-	dst = appendBinaryPackedHeader(dst, blockSize, numMiniBlocks, totalValues, firstValue)
+
+	n := len(dst)
+	dst = resize(dst, n+maxHeaderLength64)
+	dst = dst[:n+encodeBinaryPackedHeader(dst[n:], blockSize, numMiniBlocks, totalValues, firstValue)]
+
 	if totalValues < 2 {
-		return dst, nil
+		return dst
 	}
 
 	lastValue := firstValue
-	for i := 1; i < totalValues; {
-		block := make([]int64, blockSize)
-		n := blockSize
-		r := totalValues - i
-		if n > r {
-			n = r
+	for i := 1; i < len(src); i += blockSize {
+		block := [blockSize]int64{}
+		blockLength := copy(block[:], src[i:])
+
+		lastValue = blockDeltaInt64(&block, lastValue)
+		minDelta := blockMinInt64(&block)
+		blockSubInt64(&block, minDelta)
+		blockClearInt64(&block, blockLength)
+
+		bitWidths := [numMiniBlocks]byte{}
+		blockBitWidthsInt64(&bitWidths, &block)
+
+		n := len(dst)
+		dst = resize(dst, n+maxMiniBlockLength64+8)
+		n += encodeBlockHeader(dst[n:], minDelta, bitWidths)
+
+		for i, bitWidth := range bitWidths {
+			if bitWidth != 0 {
+				miniBlock := (*[miniBlockSize]int64)(block[i*miniBlockSize:])
+				miniBlockPackInt64(dst[n:], miniBlock, uint(bitWidth))
+				n += (miniBlockSize * int(bitWidth)) / 8
+			}
 		}
-		block = block[:n]
-		for j := range block {
-			block[j] = valueAt(i)
-			i++
+
+		dst = dst[:n]
+	}
+
+	return dst
+}
+
+func encodeBinaryPackedHeader(dst []byte, blockSize, numMiniBlocks, totalValues int, firstValue int64) (n int) {
+	n += binary.PutUvarint(dst[n:], uint64(blockSize))
+	n += binary.PutUvarint(dst[n:], uint64(numMiniBlocks))
+	n += binary.PutUvarint(dst[n:], uint64(totalValues))
+	n += binary.PutVarint(dst[n:], firstValue)
+	return n
+}
+
+func encodeBlockHeader(dst []byte, minDelta int64, bitWidths [numMiniBlocks]byte) (n int) {
+	n += binary.PutVarint(dst, int64(minDelta))
+	n += copy(dst[n:], bitWidths[:])
+	return n
+}
+
+func blockClearInt32(block *[blockSize]int32, blockLength int) {
+	if blockLength < blockSize {
+		clear := block[blockLength:]
+		for i := range clear {
+			clear[i] = 0
 		}
+	}
+}
 
-		for j, v := range block {
-			block[j], lastValue = v-lastValue, v
+func blockDeltaInt32(block *[blockSize]int32, lastValue int32) int32 {
+	for i, v := range block {
+		block[i], lastValue = v-lastValue, v
+	}
+	return lastValue
+}
+
+func blockMinInt32(block *[blockSize]int32) int32 {
+	min := block[0]
+	for _, v := range block[1:] {
+		if v < min {
+			min = v
 		}
+	}
+	return min
+}
 
-		minDelta := bits.MinInt64(block)
-		bits.SubInt64(block, minDelta)
-
-		// blockSize x 8: we store at most `blockSize` count of values, which
-		// might be up to 64 bits in length, which is why we multiple by 8.
-		//
-		// Technically we could size the buffer to a smaller size when the
-		// bit width requires less than 8 bytes per value, but it would cause
-		// the buffer to be put on the heap since the compiler wouldn't know
-		// how much stack space it needs in advance.
-		miniBlock := make([]byte, blockSize*8)
-		bitWidths := make([]byte, numMiniBlocks)
-		bitOffset := uint(0)
-		miniBlockLength := 0
-
-		for i := range bitWidths {
-			j := (i + 0) * miniBlockSize
-			k := (i + 1) * miniBlockSize
-
-			if k > len(block) {
-				k = len(block)
-			}
+func blockSubInt32(block *[blockSize]int32, value int32) {
+	for i := range block {
+		block[i] -= value
+	}
+}
 
-			bitWidth := uint(bits.MaxLen64(block[j:k]))
-			if bitWidth != 0 {
-				bitWidths[i] = byte(bitWidth)
-
-				for _, bits := range block[j:k] {
-					for b := uint(0); b < bitWidth; b++ {
-						x := bitOffset / 8
-						y := bitOffset % 8
-						miniBlock[x] |= byte(((bits >> b) & 1) << y)
-						bitOffset++
-					}
-				}
+func blockBitWidthsInt32(bitWidths *[numMiniBlocks]byte, block *[blockSize]int32) {
+	for i := range bitWidths {
+		j := (i + 0) * miniBlockSize
+		k := (i + 1) * miniBlockSize
+		bitWidth := 0
 
-				miniBlockLength += (miniBlockSize * int(bitWidth)) / 8
+		for _, v := range block[j:k] {
+			if n := Len32(uint32(v)); n > bitWidth {
+				bitWidth = n
 			}
+		}
 
-			if k == len(block) {
-				break
+		bitWidths[i] = byte(bitWidth)
+	}
+}
+
+func blockClearInt64(block *[blockSize]int64, blockLength int) {
+	if blockLength < blockSize {
+		clear := block[blockLength:]
+		for i := range clear {
+			clear[i] = 0
+		}
+	}
+}
+
+func blockDeltaInt64(block *[blockSize]int64, lastValue int64) int64 {
+	for i, v := range block {
+		block[i], lastValue = v-lastValue, v
+	}
+	return lastValue
+}
+
+func blockMinInt64(block *[blockSize]int64) int64 {
+	min := block[0]
+	for _, v := range block[1:] {
+		if v < min {
+			min = v
+		}
+	}
+	return min
+}
+
+func blockSubInt64(block *[blockSize]int64, value int64) {
+	for i := range block {
+		block[i] -= value
+	}
+}
+
+func blockBitWidthsInt64(bitWidths *[numMiniBlocks]byte, block *[blockSize]int64) {
+	for i := range bitWidths {
+		j := (i + 0) * miniBlockSize
+		k := (i + 1) * miniBlockSize
+		bitWidth := 0
+
+		for _, v := range block[j:k] {
+			if n := Len64(uint64(v)); n > bitWidth {
+				bitWidth = n
 			}
 		}
 
-		miniBlock = miniBlock[:miniBlockLength]
-		dst = appendBinaryPackedBlock(dst, int64(minDelta), bitWidths)
-		dst = append(dst, miniBlock...)
+		bitWidths[i] = byte(bitWidth)
+	}
+}
+
+func resize(buf []byte, size int) []byte {
+	if cap(buf) < size {
+		newCap := 2 * cap(buf)
+		if newCap < size {
+			newCap = size
+		}
+		buf = append(make([]byte, 0, newCap), buf...)
+	} else if size > len(buf) {
+		clear := buf[len(buf):size]
+		for i := range clear {
+			clear[i] = 0
+		}
+	}
+	return buf[:size]
+}
+
+type BinaryPackedEncoding struct {
+	encoding.NotSupported
+}
+
+func (e *BinaryPackedEncoding) String() string {
+	return "DELTA_BINARY_PACKED"
+}
+
+func (e *BinaryPackedEncoding) Encoding() format.Encoding {
+	return format.DeltaBinaryPacked
+}
+
+func (e *BinaryPackedEncoding) EncodeInt32(dst, src []byte) ([]byte, error) {
+	if (len(src) % 4) != 0 {
+		return dst[:0], encoding.ErrEncodeInvalidInputSize(e, "INT64", len(src))
 	}
+	return encodeInt32(dst[:0], bytesToInt32(src)), nil
+}
 
-	return dst, nil
+func (e *BinaryPackedEncoding) EncodeInt64(dst, src []byte) ([]byte, error) {
+	if (len(src) % 8) != 0 {
+		return dst[:0], encoding.ErrEncodeInvalidInputSize(e, "INT64", len(src))
+	}
+	return encodeInt64(dst[:0], bytesToInt64(src)), nil
 }
 
 func (e *BinaryPackedEncoding) DecodeInt32(dst, src []byte) ([]byte, error) {
@@ -268,24 +411,6 @@ func (e *BinaryPackedEncoding) wrap(err error) error {
 	return err
 }
 
-func appendBinaryPackedHeader(dst []byte, blockSize, numMiniBlocks, totalValues int, firstValue int64) []byte {
-	b := [4 * binary.MaxVarintLen64]byte{}
-	n := 0
-	n += binary.PutUvarint(b[n:], uint64(blockSize))
-	n += binary.PutUvarint(b[n:], uint64(numMiniBlocks))
-	n += binary.PutUvarint(b[n:], uint64(totalValues))
-	n += binary.PutVarint(b[n:], firstValue)
-	return append(dst, b[:n]...)
-}
-
-func appendBinaryPackedBlock(dst []byte, minDelta int64, bitWidths []byte) []byte {
-	b := [binary.MaxVarintLen64]byte{}
-	n := binary.PutVarint(b[:], minDelta)
-	dst = append(dst, b[:n]...)
-	dst = append(dst, bitWidths...)
-	return dst
-}
-
 func decodeBinaryPackedHeader(src []byte) (blockSize, numMiniBlocks, totalValues int, firstValue int64, next []byte, err error) {
 	u := uint64(0)
 	n := 0
diff --git a/encoding/delta/binary_packed_amd64.go b/encoding/delta/binary_packed_amd64.go
new file mode 100644
index 0000000..b151285
--- /dev/null
+++ b/encoding/delta/binary_packed_amd64.go
@@ -0,0 +1,193 @@
+//go:build !purego
+
+package delta
+
+import (
+	"golang.org/x/sys/cpu"
+)
+
+func init() {
+	if cpu.X86.HasAVX2 {
+		encodeInt32 = encodeInt32AVX2
+		encodeInt64 = encodeInt64AVX2
+	}
+}
+
+//go:noescape
+func blockDeltaInt32AVX2(block *[blockSize]int32, lastValue int32) int32
+
+//go:noescape
+func blockMinInt32AVX2(block *[blockSize]int32) int32
+
+//go:noescape
+func blockSubInt32AVX2(block *[blockSize]int32, value int32)
+
+//go:noescape
+func blockBitWidthsInt32AVX2(bitWidths *[numMiniBlocks]byte, block *[blockSize]int32)
+
+//go:noescape
+func miniBlockPackInt32Default(dst *byte, src *[miniBlockSize]int32, bitWidth uint)
+
+//go:noescape
+func miniBlockPackInt32x1bitAVX2(dst *byte, src *[miniBlockSize]int32)
+
+//go:noescape
+func miniBlockPackInt32x2bitsAVX2(dst *byte, src *[miniBlockSize]int32)
+
+//go:noescape
+func miniBlockPackInt32x3to16bitsAVX2(dst *byte, src *[miniBlockSize]int32, bitWidth uint)
+
+//go:noescape
+func miniBlockPackInt32x32bitsAVX2(dst *byte, src *[miniBlockSize]int32)
+
+func miniBlockPackInt32(dst []byte, src *[miniBlockSize]int32, bitWidth uint) {
+	miniBlockPackInt32Default(&dst[0], src, bitWidth)
+}
+
+func miniBlockPackInt32AVX2(dst *byte, src *[miniBlockSize]int32, bitWidth uint) {
+	switch {
+	case bitWidth == 1:
+		miniBlockPackInt32x1bitAVX2(dst, src)
+	case bitWidth == 2:
+		miniBlockPackInt32x2bitsAVX2(dst, src)
+	case bitWidth == 32:
+		miniBlockPackInt32x32bitsAVX2(dst, src)
+	case bitWidth <= 16:
+		miniBlockPackInt32x3to16bitsAVX2(dst, src, bitWidth)
+	default:
+		miniBlockPackInt32Default(dst, src, bitWidth)
+	}
+}
+
+func encodeInt32AVX2(dst []byte, src []int32) []byte {
+	totalValues := len(src)
+	firstValue := int32(0)
+	if totalValues > 0 {
+		firstValue = src[0]
+	}
+
+	n := len(dst)
+	dst = resize(dst, n+maxHeaderLength32)
+	dst = dst[:n+encodeBinaryPackedHeader(dst[n:], blockSize, numMiniBlocks, totalValues, int64(firstValue))]
+
+	if totalValues < 2 {
+		return dst
+	}
+
+	lastValue := firstValue
+	for i := 1; i < len(src); i += blockSize {
+		block := [blockSize]int32{}
+		blockLength := copy(block[:], src[i:])
+
+		lastValue = blockDeltaInt32AVX2(&block, lastValue)
+		minDelta := blockMinInt32AVX2(&block)
+		blockSubInt32AVX2(&block, minDelta)
+		blockClearInt32(&block, blockLength)
+
+		bitWidths := [numMiniBlocks]byte{}
+		blockBitWidthsInt32AVX2(&bitWidths, &block)
+
+		n := len(dst)
+		dst = resize(dst, n+maxMiniBlockLength32+16)
+		n += encodeBlockHeader(dst[n:], int64(minDelta), bitWidths)
+
+		for i, bitWidth := range bitWidths {
+			if bitWidth != 0 {
+				miniBlock := (*[miniBlockSize]int32)(block[i*miniBlockSize:])
+				miniBlockPackInt32AVX2(&dst[n], miniBlock, uint(bitWidth))
+				n += (miniBlockSize * int(bitWidth)) / 8
+			}
+		}
+
+		dst = dst[:n]
+	}
+
+	return dst
+}
+
+//go:noescape
+func blockDeltaInt64AVX2(block *[blockSize]int64, lastValue int64) int64
+
+//go:noescape
+func blockMinInt64AVX2(block *[blockSize]int64) int64
+
+//go:noescape
+func blockSubInt64AVX2(block *[blockSize]int64, value int64)
+
+//go:noescape
+func blockBitWidthsInt64AVX2(bitWidths *[numMiniBlocks]byte, block *[blockSize]int64)
+
+//go:noescape
+func miniBlockPackInt64Default(dst *byte, src *[miniBlockSize]int64, bitWidth uint)
+
+//go:noescape
+func miniBlockPackInt64x1bitAVX2(dst *byte, src *[miniBlockSize]int64)
+
+//go:noescape
+func miniBlockPackInt64x2bitsAVX2(dst *byte, src *[miniBlockSize]int64)
+
+//go:noescape
+func miniBlockPackInt64x64bitsAVX2(dst *byte, src *[miniBlockSize]int64)
+
+func miniBlockPackInt64(dst []byte, src *[miniBlockSize]int64, bitWidth uint) {
+	miniBlockPackInt64Default(&dst[0], src, bitWidth)
+}
+
+func miniBlockPackInt64AVX2(dst *byte, src *[miniBlockSize]int64, bitWidth uint) {
+	switch {
+	case bitWidth == 1:
+		miniBlockPackInt64x1bitAVX2(dst, src)
+	case bitWidth == 2:
+		miniBlockPackInt64x2bitsAVX2(dst, src)
+	case bitWidth == 64:
+		miniBlockPackInt64x64bitsAVX2(dst, src)
+	default:
+		miniBlockPackInt64Default(dst, src, bitWidth)
+	}
+}
+
+func encodeInt64AVX2(dst []byte, src []int64) []byte {
+	totalValues := len(src)
+	firstValue := int64(0)
+	if totalValues > 0 {
+		firstValue = src[0]
+	}
+
+	n := len(dst)
+	dst = resize(dst, n+maxHeaderLength64)
+	dst = dst[:n+encodeBinaryPackedHeader(dst[n:], blockSize, numMiniBlocks, totalValues, int64(firstValue))]
+
+	if totalValues < 2 {
+		return dst
+	}
+
+	lastValue := firstValue
+	for i := 1; i < len(src); i += blockSize {
+		block := [blockSize]int64{}
+		blockLength := copy(block[:], src[i:])
+
+		lastValue = blockDeltaInt64AVX2(&block, lastValue)
+		minDelta := blockMinInt64AVX2(&block)
+		blockSubInt64AVX2(&block, minDelta)
+		blockClearInt64(&block, blockLength)
+
+		bitWidths := [numMiniBlocks]byte{}
+		blockBitWidthsInt64AVX2(&bitWidths, &block)
+
+		n := len(dst)
+		dst = resize(dst, n+maxMiniBlockLength64+16)
+		n += encodeBlockHeader(dst[n:], int64(minDelta), bitWidths)
+
+		for i, bitWidth := range bitWidths {
+			if bitWidth != 0 {
+				miniBlock := (*[miniBlockSize]int64)(block[i*miniBlockSize:])
+				miniBlockPackInt64AVX2(&dst[n], miniBlock, uint(bitWidth))
+				n += (miniBlockSize * int(bitWidth)) / 8
+			}
+		}
+
+		dst = dst[:n]
+	}
+
+	return dst
+}
diff --git a/encoding/delta/binary_packed_amd64.s b/encoding/delta/binary_packed_amd64.s
new file mode 100644
index 0000000..4e9a191
--- /dev/null
+++ b/encoding/delta/binary_packed_amd64.s
@@ -0,0 +1,825 @@
+//go:build !purego
+
+#include "textflag.h"
+
+#define blockSize 128
+#define numMiniBlocks 4
+#define miniBlockSize 32
+
+// -----------------------------------------------------------------------------
+// 32 bits
+// -----------------------------------------------------------------------------
+
+#define deltaInt32AVX2x8(baseAddr) \
+    VMOVDQU baseAddr, Y1    \ // [0,1,2,3,4,5,6,7]
+    VPERMD Y1, Y3, Y2       \ // [7,0,1,2,3,4,5,6]
+    VPBLENDD $1, Y0, Y2, Y2 \ // [x,0,1,2,3,4,5,6]
+    VPSUBD Y2, Y1, Y2       \ // [0,1,2,...] - [x,0,1,...]
+    VMOVDQU Y2, baseAddr    \
+    VPERMD Y1, Y3, Y0
+
+// func blockDeltaInt32AVX2(block *[blockSize]int32, lastValue int32) int32
+TEXT ·blockDeltaInt32AVX2(SB), NOSPLIT, $0-24
+    MOVQ block+0(FP), AX
+    MOVL 4*blockSize-4(AX), CX
+    MOVL CX, ret+16(FP)
+
+    VPBROADCASTD lastValue+8(FP), Y0
+    VMOVDQU deltaInt32Perm<>(SB), Y3
+
+    XORQ SI, SI
+loop:
+    deltaInt32AVX2x8(0(AX)(SI*4))
+    deltaInt32AVX2x8(32(AX)(SI*4))
+    deltaInt32AVX2x8(64(AX)(SI*4))
+    deltaInt32AVX2x8(96(AX)(SI*4))
+    ADDQ $32, SI
+    CMPQ SI, $blockSize
+    JNE loop
+    VZEROUPPER
+    RET
+
+GLOBL deltaInt32Perm<>(SB), RODATA|NOPTR, $32
+DATA deltaInt32Perm<>+0(SB)/4, $7
+DATA deltaInt32Perm<>+4(SB)/4, $0
+DATA deltaInt32Perm<>+8(SB)/4, $1
+DATA deltaInt32Perm<>+12(SB)/4, $2
+DATA deltaInt32Perm<>+16(SB)/4, $3
+DATA deltaInt32Perm<>+20(SB)/4, $4
+DATA deltaInt32Perm<>+24(SB)/4, $5
+DATA deltaInt32Perm<>+28(SB)/4, $6
+
+// func blockMinInt32AVX2(block *[blockSize]int32) int32
+TEXT ·blockMinInt32AVX2(SB), NOSPLIT, $0-16
+    MOVQ block+0(FP), AX
+    VPBROADCASTD (AX), Y15
+
+    VPMINSD 0(AX), Y15, Y0
+    VPMINSD 32(AX), Y15, Y1
+    VPMINSD 64(AX), Y15, Y2
+    VPMINSD 96(AX), Y15, Y3
+    VPMINSD 128(AX), Y15, Y4
+    VPMINSD 160(AX), Y15, Y5
+    VPMINSD 192(AX), Y15, Y6
+    VPMINSD 224(AX), Y15, Y7
+    VPMINSD 256(AX), Y15, Y8
+    VPMINSD 288(AX), Y15, Y9
+    VPMINSD 320(AX), Y15, Y10
+    VPMINSD 352(AX), Y15, Y11
+    VPMINSD 384(AX), Y15, Y12
+    VPMINSD 416(AX), Y15, Y13
+    VPMINSD 448(AX), Y15, Y14
+    VPMINSD 480(AX), Y15, Y15
+
+    VPMINSD Y1, Y0, Y0
+    VPMINSD Y3, Y2, Y2
+    VPMINSD Y5, Y4, Y4
+    VPMINSD Y7, Y6, Y6
+    VPMINSD Y9, Y8, Y8
+    VPMINSD Y11, Y10, Y10
+    VPMINSD Y13, Y12, Y12
+    VPMINSD Y15, Y14, Y14
+
+    VPMINSD Y2, Y0, Y0
+    VPMINSD Y6, Y4, Y4
+    VPMINSD Y10, Y8, Y8
+    VPMINSD Y14, Y12, Y12
+
+    VPMINSD Y4, Y0, Y0
+    VPMINSD Y12, Y8, Y8
+
+    VPMINSD Y8, Y0, Y0
+
+    VPERM2I128 $1, Y0, Y0, Y1
+    VPMINSD Y1, Y0, Y0
+
+    VPSHUFD $0b00011011, Y0, Y1
+    VPMINSD Y1, Y0, Y0
+    VZEROUPPER
+
+    MOVQ X0, CX
+    MOVL CX, BX
+    SHRQ $32, CX
+    CMPL CX, BX
+    CMOVLLT CX, BX
+    MOVL BX, ret+8(FP)
+    RET
+
+#define subInt32AVX2x32(baseAddr, offset) \
+    VMOVDQU offset+0(baseAddr), Y1      \
+    VMOVDQU offset+32(baseAddr), Y2     \
+    VMOVDQU offset+64(baseAddr), Y3     \
+    VMOVDQU offset+96(baseAddr), Y4     \
+    VPSUBD Y0, Y1, Y1                   \
+    VPSUBD Y0, Y2, Y2                   \
+    VPSUBD Y0, Y3, Y3                   \
+    VPSUBD Y0, Y4, Y4                   \
+    VMOVDQU Y1, offset+0(baseAddr)      \
+    VMOVDQU Y2, offset+32(baseAddr)     \
+    VMOVDQU Y3, offset+64(baseAddr)     \
+    VMOVDQU Y4, offset+96(baseAddr)
+
+// func blockSubInt32AVX2(block *[blockSize]int32, value int32)
+TEXT ·blockSubInt32AVX2(SB), NOSPLIT, $0-16
+    MOVQ block+0(FP), AX
+    VPBROADCASTD value+8(FP), Y0
+    subInt32AVX2x32(AX, 0)
+    subInt32AVX2x32(AX, 128)
+    subInt32AVX2x32(AX, 256)
+    subInt32AVX2x32(AX, 384)
+    VZEROUPPER
+    RET
+
+// func blockBitWidthsInt32AVX2(bitWidths *[numMiniBlocks]byte, block *[blockSize]int32)
+TEXT ·blockBitWidthsInt32AVX2(SB), NOSPLIT, $0-16
+    MOVQ bitWidths+0(FP), AX
+    MOVQ block+8(FP), BX
+
+    // AVX2 only has signed comparisons (and min/max), we emulate working on
+    // unsigned values by adding -2^31 to the values. Y5 is a vector of -2^31
+    // used to offset 8 packed 32 bits integers in other YMM registers where
+    // the block data are loaded.
+    VPCMPEQD Y5, Y5, Y5
+    VPSLLD $31, Y5, Y5
+
+    XORQ DI, DI
+loop:
+    VPBROADCASTD (BX), Y0 // max
+    VPADDD Y5, Y0, Y0
+
+    VMOVDQU (BX), Y1
+    VMOVDQU 32(BX), Y2
+    VMOVDQU 64(BX), Y3
+    VMOVDQU 96(BX), Y4
+
+    VPADDD Y5, Y1, Y1
+    VPADDD Y5, Y2, Y2
+    VPADDD Y5, Y3, Y3
+    VPADDD Y5, Y4, Y4
+
+    VPMAXSD Y2, Y1, Y1
+    VPMAXSD Y4, Y3, Y3
+    VPMAXSD Y3, Y1, Y1
+    VPMAXSD Y1, Y0, Y0
+
+    VPERM2I128 $1, Y0, Y0, Y1
+    VPMAXSD Y1, Y0, Y0
+
+    VPSHUFD $0b00011011, Y0, Y1
+    VPMAXSD Y1, Y0, Y0
+    VPSUBD Y5, Y0, Y0
+
+    MOVQ X0, CX
+    MOVL CX, DX
+    SHRQ $32, CX
+    CMPL CX, DX
+    CMOVLHI CX, DX
+
+    LZCNTL DX, DX
+    NEGL DX
+    ADDL $32, DX
+    MOVB DX, (AX)(DI*1)
+
+    ADDQ $128, BX
+    INCQ DI
+    CMPQ DI, $numMiniBlocks
+    JNE loop
+    VZEROUPPER
+    RET
+
+// miniBlockPackInt32Default is the generic implementation of the algorithm to
+// pack 32 bit integers into values of a given bit width (<=32).
+//
+// This algorithm is much slower than the vectorized versions, but is useful
+// as a reference implementation to run the tests against, and as fallback when
+// the code runs on a CPU which does not support the AVX2 instruction set.
+//
+// func miniBlockPackInt32Default(dst *byte, src *[miniBlockSize]int32, bitWidth uint)
+TEXT ·miniBlockPackInt32Default(SB), NOSPLIT, $0-24
+    MOVQ dst+0(FP), AX
+    MOVQ src+8(FP), BX
+    MOVQ bitWidth+16(FP), R9
+
+    XORQ DI, DI // bitOffset
+    XORQ SI, SI
+loop:
+    MOVQ DI, CX
+    MOVQ DI, DX
+
+    ANDQ $0b11111, CX // bitOffset % 32
+    SHRQ $5, DX       // bitOffset / 32
+
+    MOVLQZX (BX)(SI*4), R8
+    SHLQ CX, R8
+    ORQ R8, (AX)(DX*4)
+
+    ADDQ R9, DI
+    INCQ SI
+    CMPQ SI, $miniBlockSize
+    JNE loop
+    RET
+
+// miniBlockPackInt32x1bitAVX2 packs 32 bit integers into 1 bit values in the
+// the output buffer.
+//
+// The algorithm uses MOVMSKPS to extract the 8 relevant bits from the 8 values
+// packed in YMM registers, then combines 4 of these into a 32 bit word which
+// then gets written to the output. The result is 32 bits because each mini
+// block has 32 values (the block size is 128 and there are 4 mini blocks per
+// block).
+//
+// func miniBlockPackInt32x1bitAVX2(dst *byte, src *[miniBlockSize]int32)
+TEXT ·miniBlockPackInt32x1bitAVX2(SB), NOSPLIT, $0-16
+    MOVQ dst+0(FP), AX
+    MOVQ src+8(FP), BX
+
+    VMOVDQU 0(BX), Y0
+    VMOVDQU 32(BX), Y1
+    VMOVDQU 64(BX), Y2
+    VMOVDQU 96(BX), Y3
+
+    VPSLLD $31, Y0, Y0
+    VPSLLD $31, Y1, Y1
+    VPSLLD $31, Y2, Y2
+    VPSLLD $31, Y3, Y3
+
+    VMOVMSKPS Y0, R8
+    VMOVMSKPS Y1, R9
+    VMOVMSKPS Y2, R10
+    VMOVMSKPS Y3, R11
+
+    SHLL $8, R9
+    SHLL $16, R10
+    SHLL $24, R11
+
+    ORL R9, R8
+    ORL R10, R8
+    ORL R11, R8
+    MOVL R8, (AX)
+    VZEROUPPER
+    RET
+
+// miniBlockPackInt32x2bitsAVX2 implements an algorithm for packing 32 bit
+// integers into 2 bit values.
+//
+// The algorithm is derived from the one employed in miniBlockPackInt32x1bitAVX2
+// but needs to perform a bit extra work since MOVMSKPS can only extract one bit
+// per packed integer of each YMM vector. We run two passes to extract the two
+// bits needed to compose each item of the result, and merge the values by
+// interleaving the first and second bits with PDEP.
+//
+// func miniBlockPackInt32x2bitsAVX2(dst *byte, src *[miniBlockSize]int32)
+TEXT ·miniBlockPackInt32x2bitsAVX2(SB), NOSPLIT, $0-16
+    MOVQ dst+0(FP), AX
+    MOVQ src+8(FP), BX
+
+    VMOVDQU 0(BX), Y0
+    VMOVDQU 32(BX), Y1
+    VMOVDQU 64(BX), Y2
+    VMOVDQU 96(BX), Y3
+
+    VPSLLD $31, Y0, Y4
+    VPSLLD $31, Y1, Y5
+    VPSLLD $31, Y2, Y6
+    VPSLLD $31, Y3, Y7
+
+    VMOVMSKPS Y4, R8
+    VMOVMSKPS Y5, R9
+    VMOVMSKPS Y6, R10
+    VMOVMSKPS Y7, R11
+
+    SHLQ $8, R9
+    SHLQ $16, R10
+    SHLQ $24, R11
+    ORQ R9, R8
+    ORQ R10, R8
+    ORQ R11, R8
+
+    MOVQ $0x5555555555555555, DX // 0b010101...
+    PDEPQ DX, R8, R8
+
+    VPSLLD $30, Y0, Y8
+    VPSLLD $30, Y1, Y9
+    VPSLLD $30, Y2, Y10
+    VPSLLD $30, Y3, Y11
+
+    VMOVMSKPS Y8, R12
+    VMOVMSKPS Y9, R13
+    VMOVMSKPS Y10, R14
+    VMOVMSKPS Y11, R15
+
+    SHLQ $8, R13
+    SHLQ $16, R14
+    SHLQ $24, R15
+    ORQ R13, R12
+    ORQ R14, R12
+    ORQ R15, R12
+
+    MOVQ $0xAAAAAAAAAAAAAAAA, DI // 0b101010...
+    PDEPQ DI, R12, R12
+
+    ORQ R12, R8
+    MOVQ R8, (AX)
+    VZEROUPPER
+    RET
+
+// miniBlockPackInt32x32bitsAVX2 is a specialization of the bit packing logic
+// for 32 bit integers when the output bit width is also 32, in which case a
+// simple copy of the mini block to the output buffer produces the result.
+//
+// func miniBlockPackInt32x32bitsAVX2(dst *byte, src *[miniBlockSize]int32)
+TEXT ·miniBlockPackInt32x32bitsAVX2(SB), NOSPLIT, $0-16
+    MOVQ dst+0(FP), AX
+    MOVQ src+8(FP), BX
+    VMOVDQU 0(BX), Y0
+    VMOVDQU 32(BX), Y1
+    VMOVDQU 64(BX), Y2
+    VMOVDQU 96(BX), Y3
+    VMOVDQU Y0, 0(AX)
+    VMOVDQU Y1, 32(AX)
+    VMOVDQU Y2, 64(AX)
+    VMOVDQU Y3, 96(AX)
+    VZEROUPPER
+    RET
+
+// miniBlockPackInt32x3to16bitsAVX2 is the algorithm used to bit-pack 32 bit
+// integers into values of width 3 to 16 bits.
+//
+// This function is a small overhead due to having to initialize registers with
+// values that depend on the bit width. We measured this cost at ~10% throughput
+// in synthetic benchmarks compared to generating constant shifts and offsets
+// using a macro. Using a single function rather than generating one for each
+// bit width has the benefit of reducing the code size, which in practice can
+// also yield benefits like reducing CPU cache misses. Not using a macro also
+// has other advantages like providing accurate line number of stack traces and
+// enabling the use of breakpoints when debugging. Overall, this approach seemed
+// to be the right trade off between performance and maintainability.
+//
+// The algorithm treats chunks of 8 values in 4 iterations to process all 32
+// values of the mini block. Writes to the output buffer are aligned on 128 bits
+// since we may write up to 128 bits (8 x 16 bits). Padding is therefore
+// required in the output buffer to avoid triggering a segfault.
+// The encodeInt32AVX2 method adds enough padding when sizing the output buffer
+// to account for this requirement.
+//
+// We leverage the two lanes of YMM registers to work on two sets of 4 values
+// (in the sequence of VMOVDQU/VPSHUFD, VPAND, VPSLLQ, VPOR), resulting in having
+// two sets of bit-packed values in the lower 64 bits of each YMM lane.
+// The upper lane is then permuted into a lower lane to merge the two results,
+// which may not be aligned on byte boundaries so we shift the lower and upper
+// bits and compose two sets of 128 bits sequences (VPSLLQ, VPSRLQ, VBLENDPD),
+// merge them and write the 16 bytes result to the output buffer.
+TEXT ·miniBlockPackInt32x3to16bitsAVX2(SB), NOSPLIT, $0-24
+    MOVQ dst+0(FP), AX
+    MOVQ src+8(FP), BX
+
+    VPBROADCASTQ bitWidths+16(FP), Y6 // [1*bitWidth...]
+    VPSLLQ $1, Y6, Y7                 // [2*bitWidth...]
+    VPADDQ Y6, Y7, Y8                 // [3*bitWidth...]
+    VPSLLQ $2, Y6, Y9                 // [4*bitWidth...]
+
+    VPBROADCASTQ sixtyfour<>(SB), Y10
+    VPSUBQ Y6, Y10, Y11 // [64-1*bitWidth...]
+    VPSUBQ Y9, Y10, Y12 // [64-4*bitWidth...]
+    VPCMPEQQ Y4, Y4, Y4
+    VPSRLVQ Y11, Y4, Y4
+
+    VPXOR Y5, Y5, Y5
+    XORQ SI, SI
+loop:
+    VMOVDQU (BX)(SI*4), Y0
+    VPSHUFD $0b01010101, Y0, Y1
+    VPSHUFD $0b10101010, Y0, Y2
+    VPSHUFD $0b11111111, Y0, Y3
+
+    VPAND Y4, Y0, Y0
+    VPAND Y4, Y1, Y1
+    VPAND Y4, Y2, Y2
+    VPAND Y4, Y3, Y3
+
+    VPSLLVQ Y6, Y1, Y1
+    VPSLLVQ Y7, Y2, Y2
+    VPSLLVQ Y8, Y3, Y3
+
+    VPOR Y1, Y0, Y0
+    VPOR Y3, Y2, Y2
+    VPOR Y2, Y0, Y0
+
+    VPERMQ $0b00001010, Y0, Y1
+
+    VPSLLVQ X9, X1, X2
+    VPSRLQ X12, X1, X3
+    VBLENDPD $0b10, X3, X2, X1
+    VBLENDPD $0b10, X5, X0, X0
+    VPOR X1, X0, X0
+
+    VMOVDQU X0, (AX)
+
+    ADDQ CX, AX
+    ADDQ $8, SI
+    CMPQ SI, $miniBlockSize
+    JNE loop
+    VZEROUPPER
+    RET
+
+GLOBL sixtyfour<>(SB), RODATA|NOPTR, $32
+DATA sixtyfour<>+0(SB)/8, $64
+DATA sixtyfour<>+8(SB)/8, $64
+DATA sixtyfour<>+16(SB)/8, $64
+DATA sixtyfour<>+24(SB)/8, $64
+
+// -----------------------------------------------------------------------------
+// 64 bits
+// -----------------------------------------------------------------------------
+
+#define deltaInt64AVX2x4(baseAddr)  \
+    VMOVDQU baseAddr, Y1            \ // [0,1,2,3]
+    VPERMQ $0b10010011, Y1, Y2      \ // [3,0,1,2]
+    VPBLENDD $3, Y0, Y2, Y2         \ // [x,0,1,2]
+    VPSUBQ Y2, Y1, Y2               \ // [0,1,2,3] - [x,0,1,2]
+    VMOVDQU Y2, baseAddr            \
+    VPERMQ $0b10010011, Y1, Y0
+
+// func blockDeltaInt64AVX2(block *[blockSize]int64, lastValue int64) int64
+TEXT ·blockDeltaInt64AVX2(SB), NOSPLIT, $0-24
+    MOVQ block+0(FP), AX
+    MOVQ 8*blockSize-8(AX), CX
+    MOVQ CX, ret+16(FP)
+
+    VPBROADCASTQ lastValue+8(FP), Y0
+    XORQ SI, SI
+loop:
+    deltaInt64AVX2x4((AX)(SI*8))
+    deltaInt64AVX2x4(32(AX)(SI*8))
+    deltaInt64AVX2x4(64(AX)(SI*8))
+    deltaInt64AVX2x4(96(AX)(SI*8))
+    ADDQ $16, SI
+    CMPQ SI, $blockSize
+    JNE loop
+    VZEROUPPER
+    RET
+
+// vpminsq is an emulation of the AVX-512 VPMINSQ instruction with AVX2.
+#define vpminsq(ones, tmp, arg2, arg1, ret) \
+    VPCMPGTQ arg1, arg2, tmp \
+    VPBLENDVB tmp, arg1, arg2, ret
+
+// func blockMinInt64AVX2(block *[blockSize]int64) int64
+TEXT ·blockMinInt64AVX2(SB), NOSPLIT, $0-16
+    MOVQ block+0(FP), AX
+    XORQ SI, SI
+    VPCMPEQQ Y9, Y9, Y9 // ones
+    VPBROADCASTQ (AX), Y0
+loop:
+    VMOVDQU 0(AX)(SI*8), Y1
+    VMOVDQU 32(AX)(SI*8), Y2
+    VMOVDQU 64(AX)(SI*8), Y3
+    VMOVDQU 96(AX)(SI*8), Y4
+    VMOVDQU 128(AX)(SI*8), Y5
+    VMOVDQU 160(AX)(SI*8), Y6
+    VMOVDQU 192(AX)(SI*8), Y7
+    VMOVDQU 224(AX)(SI*8), Y8
+
+    vpminsq(Y9, Y10, Y0, Y1, Y1)
+    vpminsq(Y9, Y11, Y0, Y2, Y2)
+    vpminsq(Y9, Y12, Y0, Y3, Y3)
+    vpminsq(Y9, Y13, Y0, Y4, Y4)
+    vpminsq(Y9, Y14, Y0, Y5, Y5)
+    vpminsq(Y9, Y15, Y0, Y6, Y6)
+    vpminsq(Y9, Y10, Y0, Y7, Y7)
+    vpminsq(Y9, Y11, Y0, Y8, Y8)
+
+    vpminsq(Y9, Y12, Y2, Y1, Y1)
+    vpminsq(Y9, Y13, Y4, Y3, Y3)
+    vpminsq(Y9, Y14, Y6, Y5, Y5)
+    vpminsq(Y9, Y15, Y8, Y7, Y7)
+
+    vpminsq(Y9, Y10, Y3, Y1, Y1)
+    vpminsq(Y9, Y11, Y7, Y5, Y5)
+    vpminsq(Y9, Y12, Y5, Y1, Y0)
+
+    ADDQ $32, SI
+    CMPQ SI, $blockSize
+    JNE loop
+
+    VPERM2I128 $1, Y0, Y0, Y1
+    vpminsq(Y9, Y10, Y1, Y0, Y0)
+
+    MOVQ X0, CX
+    VPEXTRQ $1, X0, BX
+    CMPQ CX, BX
+    CMOVQLT CX, BX
+    MOVQ BX, ret+8(FP)
+    VZEROUPPER
+    RET
+
+#define subInt64AVX2x32(baseAddr, offset) \
+    VMOVDQU offset+0(baseAddr), Y1      \
+    VMOVDQU offset+32(baseAddr), Y2     \
+    VMOVDQU offset+64(baseAddr), Y3     \
+    VMOVDQU offset+96(baseAddr), Y4     \
+    VMOVDQU offset+128(baseAddr), Y5    \
+    VMOVDQU offset+160(baseAddr), Y6    \
+    VMOVDQU offset+192(baseAddr), Y7    \
+    VMOVDQU offset+224(baseAddr), Y8    \
+    VPSUBQ Y0, Y1, Y1                   \
+    VPSUBQ Y0, Y2, Y2                   \
+    VPSUBQ Y0, Y3, Y3                   \
+    VPSUBQ Y0, Y4, Y4                   \
+    VPSUBQ Y0, Y5, Y5                   \
+    VPSUBQ Y0, Y6, Y6                   \
+    VPSUBQ Y0, Y7, Y7                   \
+    VPSUBQ Y0, Y8, Y8                   \
+    VMOVDQU Y1, offset+0(baseAddr)      \
+    VMOVDQU Y2, offset+32(baseAddr)     \
+    VMOVDQU Y3, offset+64(baseAddr)     \
+    VMOVDQU Y4, offset+96(baseAddr)     \
+    VMOVDQU Y5, offset+128(baseAddr)    \
+    VMOVDQU Y6, offset+160(baseAddr)    \
+    VMOVDQU Y7, offset+192(baseAddr)    \
+    VMOVDQU Y8, offset+224(baseAddr)
+
+// func blockSubInt64AVX2(block *[blockSize]int64, value int64)
+TEXT ·blockSubInt64AVX2(SB), NOSPLIT, $0-16
+    MOVQ block+0(FP), AX
+    VPBROADCASTQ value+8(FP), Y0
+    subInt64AVX2x32(AX, 0)
+    subInt64AVX2x32(AX, 256)
+    subInt64AVX2x32(AX, 512)
+    subInt64AVX2x32(AX, 768)
+    VZEROUPPER
+    RET
+
+// func blockBitWidthsInt64AVX2(bitWidths *[numMiniBlocks]byte, block *[blockSize]int64)
+TEXT ·blockBitWidthsInt64AVX2(SB), NOSPLIT, $0-16
+    MOVQ bitWidths+0(FP), AX
+    MOVQ block+8(FP), BX
+
+    // AVX2 only has signed comparisons (and min/max), we emulate working on
+    // unsigned values by adding -2^64 to the values. Y9 is a vector of -2^64
+    // used to offset 4 packed 64 bits integers in other YMM registers where
+    // the block data are loaded.
+    VPCMPEQQ Y9, Y9, Y9
+    VPSLLQ $63, Y9, Y9
+
+    XORQ DI, DI
+loop:
+    VPBROADCASTQ (BX), Y0 // max
+    VPADDQ Y9, Y0, Y0
+
+    VMOVDQU (BX), Y1
+    VMOVDQU 32(BX), Y2
+    VMOVDQU 64(BX), Y3
+    VMOVDQU 96(BX), Y4
+    VMOVDQU 128(BX), Y5
+    VMOVDQU 160(BX), Y6
+    VMOVDQU 192(BX), Y7
+    VMOVDQU 224(BX), Y8
+
+    VPADDQ Y9, Y1, Y1
+    VPADDQ Y9, Y2, Y2
+    VPADDQ Y9, Y3, Y3
+    VPADDQ Y9, Y4, Y4
+    VPADDQ Y9, Y5, Y5
+    VPADDQ Y9, Y6, Y6
+    VPADDQ Y9, Y7, Y7
+    VPADDQ Y9, Y8, Y8
+
+    VPMAXSQ Y2, Y1, Y1
+    VPMAXSQ Y4, Y3, Y3
+    VPMAXSQ Y6, Y5, Y5
+    VPMAXSQ Y8, Y7, Y7
+
+    VPMAXSQ Y3, Y1, Y1
+    VPMAXSQ Y7, Y5, Y5
+    VPMAXSQ Y5, Y1, Y1
+    VPMAXSQ Y1, Y0, Y0
+
+    VPERM2I128 $1, Y0, Y0, Y1
+    VPMAXSQ Y1, Y0, Y0
+    VPSUBQ Y9, Y0, Y0
+
+    MOVQ X0, CX
+    VPEXTRQ $1, X0, DX
+    CMPQ CX, DX
+    CMOVQHI CX, DX
+
+    LZCNTQ DX, DX
+    NEGQ DX
+    ADDQ $64, DX
+    MOVB DX, (AX)(DI*1)
+
+    ADDQ $256, BX
+    INCQ DI
+    CMPQ DI, $numMiniBlocks
+    JNE loop
+    VZEROUPPER
+    RET
+
+// miniBlockPackInt64Default is the generic implementation of the algorithm to
+// pack 64 bit integers into values of a given bit width (<=64).
+//
+// This algorithm is much slower than the vectorized versions, but is useful
+// as a reference implementation to run the tests against, and as fallback when
+// the code runs on a CPU which does not support the AVX2 instruction set.
+//
+// func miniBlockPackInt64Default(dst *byte, src *[miniBlockSize]int64, bitWidth uint)
+TEXT ·miniBlockPackInt64Default(SB), NOSPLIT, $0-24
+    MOVQ dst+0(FP), AX
+    MOVQ src+8(FP), BX
+    MOVQ bitWidth+16(FP), R10
+
+    XORQ R11, R11 // zero
+    XORQ DI, DI // bitOffset
+    XORQ SI, SI
+loop:
+    MOVQ DI, CX
+    MOVQ DI, DX
+
+    ANDQ $0b111111, CX // bitOffset % 64
+    SHRQ $6, DX        // bitOffset / 64
+
+    MOVQ (BX)(SI*8), R8
+    MOVQ R8, R9
+    SHLQ CX, R8
+    NEGQ CX
+    ADDQ $64, CX
+    SHRQ CX, R9
+    CMPQ CX, $64
+    CMOVQEQ R11, R9 // needed because shifting by more than 63 is undefined
+
+    ORQ R8, 0(AX)(DX*8)
+    ORQ R9, 8(AX)(DX*8)
+
+    ADDQ R10, DI
+    INCQ SI
+    CMPQ SI, $miniBlockSize
+    JNE loop
+    RET
+
+// func miniBlockPackInt64x1bitAVX2(dst *byte, src *[miniBlockSize]int64)
+TEXT ·miniBlockPackInt64x1bitAVX2(SB), NOSPLIT, $0-16
+    MOVQ dst+0(FP), AX
+    MOVQ src+8(FP), BX
+
+    VMOVDQU 0(BX), Y0
+    VMOVDQU 32(BX), Y1
+    VMOVDQU 64(BX), Y2
+    VMOVDQU 96(BX), Y3
+    VMOVDQU 128(BX), Y4
+    VMOVDQU 160(BX), Y5
+    VMOVDQU 192(BX), Y6
+    VMOVDQU 224(BX), Y7
+
+    VPSLLQ $63, Y0, Y0
+    VPSLLQ $63, Y1, Y1
+    VPSLLQ $63, Y2, Y2
+    VPSLLQ $63, Y3, Y3
+    VPSLLQ $63, Y4, Y4
+    VPSLLQ $63, Y5, Y5
+    VPSLLQ $63, Y6, Y6
+    VPSLLQ $63, Y7, Y7
+
+    VMOVMSKPD Y0, R8
+    VMOVMSKPD Y1, R9
+    VMOVMSKPD Y2, R10
+    VMOVMSKPD Y3, R11
+    VMOVMSKPD Y4, R12
+    VMOVMSKPD Y5, R13
+    VMOVMSKPD Y6, R14
+    VMOVMSKPD Y7, R15
+
+    SHLL $4, R9
+    SHLL $8, R10
+    SHLL $12, R11
+    SHLL $16, R12
+    SHLL $20, R13
+    SHLL $24, R14
+    SHLL $28, R15
+
+    ORL R9, R8
+    ORL R11, R10
+    ORL R13, R12
+    ORL R15, R14
+    ORL R10, R8
+    ORL R14, R12
+    ORL R12, R8
+
+    MOVL R8, (AX)
+    VZEROUPPER
+    RET
+
+// func miniBlockPackInt64x2bitsAVX2(dst *byte, src *[miniBlockSize]int64)
+TEXT ·miniBlockPackInt64x2bitsAVX2(SB), NOSPLIT, $0-16
+    MOVQ dst+0(FP), AX
+    MOVQ src+8(FP), BX
+
+    VMOVDQU 0(BX), Y8
+    VMOVDQU 32(BX), Y9
+    VMOVDQU 64(BX), Y10
+    VMOVDQU 96(BX), Y11
+    VMOVDQU 128(BX), Y12
+    VMOVDQU 160(BX), Y13
+    VMOVDQU 192(BX), Y14
+    VMOVDQU 224(BX), Y15
+
+    VPSLLQ $63, Y8, Y0
+    VPSLLQ $63, Y9, Y1
+    VPSLLQ $63, Y10, Y2
+    VPSLLQ $63, Y11, Y3
+    VPSLLQ $63, Y12, Y4
+    VPSLLQ $63, Y13, Y5
+    VPSLLQ $63, Y14, Y6
+    VPSLLQ $63, Y15, Y7
+
+    VMOVMSKPD Y0, R8
+    VMOVMSKPD Y1, R9
+    VMOVMSKPD Y2, R10
+    VMOVMSKPD Y3, R11
+    VMOVMSKPD Y4, R12
+    VMOVMSKPD Y5, R13
+    VMOVMSKPD Y6, R14
+    VMOVMSKPD Y7, R15
+
+    SHLQ $4, R9
+    SHLQ $8, R10
+    SHLQ $12, R11
+    SHLQ $16, R12
+    SHLQ $20, R13
+    SHLQ $24, R14
+    SHLQ $28, R15
+
+    ORQ R9, R8
+    ORQ R11, R10
+    ORQ R13, R12
+    ORQ R15, R14
+    ORQ R10, R8
+    ORQ R14, R12
+    ORQ R12, R8
+
+    MOVQ $0x5555555555555555, CX // 0b010101...
+    PDEPQ CX, R8, CX
+
+    VPSLLQ $62, Y8, Y8
+    VPSLLQ $62, Y9, Y9
+    VPSLLQ $62, Y10, Y10
+    VPSLLQ $62, Y11, Y11
+    VPSLLQ $62, Y12, Y12
+    VPSLLQ $62, Y13, Y13
+    VPSLLQ $62, Y14, Y14
+    VPSLLQ $62, Y15, Y15
+
+    VMOVMSKPD Y8, R8
+    VMOVMSKPD Y9, R9
+    VMOVMSKPD Y10, R10
+    VMOVMSKPD Y11, R11
+    VMOVMSKPD Y12, R12
+    VMOVMSKPD Y13, R13
+    VMOVMSKPD Y14, R14
+    VMOVMSKPD Y15, R15
+
+    SHLQ $4, R9
+    SHLQ $8, R10
+    SHLQ $12, R11
+    SHLQ $16, R12
+    SHLQ $20, R13
+    SHLQ $24, R14
+    SHLQ $28, R15
+
+    ORQ R9, R8
+    ORQ R11, R10
+    ORQ R13, R12
+    ORQ R15, R14
+    ORQ R10, R8
+    ORQ R14, R12
+    ORQ R12, R8
+
+    MOVQ $0xAAAAAAAAAAAAAAAA, DX // 0b101010...
+    PDEPQ DX, R8, DX
+    ORQ DX, CX
+    MOVQ CX, (AX)
+    VZEROUPPER
+    RET
+
+// func miniBlockPackInt64x64bitsAVX2(dst *byte, src *[miniBlockSize]int64)
+TEXT ·miniBlockPackInt64x64bitsAVX2(SB), NOSPLIT, $0-16
+    MOVQ dst+0(FP), AX
+    MOVQ src+8(FP), BX
+    VMOVDQU 0(BX), Y0
+    VMOVDQU 32(BX), Y1
+    VMOVDQU 64(BX), Y2
+    VMOVDQU 96(BX), Y3
+    VMOVDQU 128(BX), Y4
+    VMOVDQU 160(BX), Y5
+    VMOVDQU 192(BX), Y6
+    VMOVDQU 224(BX), Y7
+    VMOVDQU Y0, 0(AX)
+    VMOVDQU Y1, 32(AX)
+    VMOVDQU Y2, 64(AX)
+    VMOVDQU Y3, 96(AX)
+    VMOVDQU Y4, 128(AX)
+    VMOVDQU Y5, 160(AX)
+    VMOVDQU Y6, 192(AX)
+    VMOVDQU Y7, 224(AX)
+    VZEROUPPER
+    RET
diff --git a/encoding/delta/binary_packed_purego.go b/encoding/delta/binary_packed_purego.go
new file mode 100644
index 0000000..66d24d9
--- /dev/null
+++ b/encoding/delta/binary_packed_purego.go
@@ -0,0 +1,49 @@
+//go:build purego || !amd64
+
+package delta
+
+import (
+	"encoding/binary"
+)
+
+func miniBlockPackInt32(dst []byte, src *[miniBlockSize]int32, bitWidth uint) {
+	bitMask := uint32(1<<bitWidth) - 1
+	bitOffset := uint(0)
+
+	for _, value := range src {
+		i := bitOffset / 32
+		j := bitOffset % 32
+
+		lo := binary.LittleEndian.Uint32(dst[(i+0)*4:])
+		hi := binary.LittleEndian.Uint32(dst[(i+1)*4:])
+
+		lo |= (uint32(value) & bitMask) << j
+		hi |= (uint32(value) >> (32 - j))
+
+		binary.LittleEndian.PutUint32(dst[(i+0)*4:], lo)
+		binary.LittleEndian.PutUint32(dst[(i+1)*4:], hi)
+
+		bitOffset += bitWidth
+	}
+}
+
+func miniBlockPackInt64(dst []byte, src *[miniBlockSize]int64, bitWidth uint) {
+	bitMask := uint64(1<<bitWidth) - 1
+	bitOffset := uint(0)
+
+	for _, value := range src {
+		i := bitOffset / 64
+		j := bitOffset % 64
+
+		lo := binary.LittleEndian.Uint64(dst[(i+0)*8:])
+		hi := binary.LittleEndian.Uint64(dst[(i+1)*8:])
+
+		lo |= (uint64(value) & bitMask) << j
+		hi |= (uint64(value) >> (64 - j))
+
+		binary.LittleEndian.PutUint64(dst[(i+0)*8:], lo)
+		binary.LittleEndian.PutUint64(dst[(i+1)*8:], hi)
+
+		bitOffset += bitWidth
+	}
+}
diff --git a/encoding/delta/byte_array.go b/encoding/delta/byte_array.go
index 83ec707..98b9a3e 100644
--- a/encoding/delta/byte_array.go
+++ b/encoding/delta/byte_array.go
@@ -3,13 +3,16 @@ package delta
 import (
 	"bytes"
 	"fmt"
-	"math"
 
 	"github.com/segmentio/parquet-go/encoding"
 	"github.com/segmentio/parquet-go/encoding/plain"
 	"github.com/segmentio/parquet-go/format"
 )
 
+const (
+	maxLinearSearchPrefixLength = 128 // arbitrary
+)
+
 type ByteArrayEncoding struct {
 	encoding.NotSupported
 }
@@ -23,27 +26,64 @@ func (e *ByteArrayEncoding) Encoding() format.Encoding {
 }
 
 func (e *ByteArrayEncoding) EncodeByteArray(dst, src []byte) ([]byte, error) {
-	lastOffset := int32(0)
+	prefix := getInt32Buffer()
+	defer putInt32Buffer(prefix)
+
+	length := getInt32Buffer()
+	defer putInt32Buffer(length)
 
-	offset := getInt32Buffer()
-	defer putInt32Buffer(offset)
+	totalSize := 0
+	lastValue := ([]byte)(nil)
 
-	err := plain.RangeByteArrays(src, func(value []byte) error {
-		offset.values = append(offset.values, lastOffset)
-		lastOffset += 4 + int32(len(value))
-		return nil
-	})
-	if err != nil {
-		return dst[:0], encoding.Error(e, err)
+	for i := 0; i < len(src); {
+		r := len(src) - i
+		if r < plain.ByteArrayLengthSize {
+			return dst[:0], plain.ErrTooShort(r)
+		}
+		n := plain.ByteArrayLength(src[i:])
+		i += plain.ByteArrayLengthSize
+		r -= plain.ByteArrayLengthSize
+		if n > r {
+			return dst[:0], plain.ErrTooShort(n)
+		}
+		if n > plain.MaxByteArrayLength {
+			return dst[:0], plain.ErrTooLarge(n)
+		}
+		v := src[i : i+n : i+n]
+		p := 0
+
+		if len(v) <= maxLinearSearchPrefixLength {
+			p = linearSearchPrefixLength(lastValue, v)
+		} else {
+			p = binarySearchPrefixLength(len(lastValue)/2, lastValue, v)
+		}
+
+		prefix.values = append(prefix.values, int32(p))
+		length.values = append(length.values, int32(n-p))
+		lastValue = v
+		totalSize += n - p
+		i += n
 	}
 
-	return e.encode(dst[:0], len(offset.values), func(i int) []byte {
-		j := int(offset.values[i])
-		k := j + plain.ByteArrayLength(src[j:])
-		j += 4
-		k += 4
-		return src[j:k:k]
-	})
+	dst = dst[:0]
+	dst = encodeInt32(dst, prefix.values)
+	dst = encodeInt32(dst, length.values)
+	dst = resize(dst, len(dst)+totalSize)
+
+	b := dst[len(dst)-totalSize:]
+	i := plain.ByteArrayLengthSize
+	j := 0
+
+	_ = length.values[:len(prefix.values)]
+
+	for k, p := range prefix.values {
+		n := p + length.values[k]
+		j += copy(b[j:], src[i+int(p):i+int(n)])
+		i += plain.ByteArrayLengthSize
+		i += int(n)
+	}
+
+	return dst, nil
 }
 
 func (e *ByteArrayEncoding) EncodeFixedLenByteArray(dst, src []byte, size int) ([]byte, error) {
@@ -57,45 +97,40 @@ func (e *ByteArrayEncoding) EncodeFixedLenByteArray(dst, src []byte, size int) (
 	if (len(src) % size) != 0 {
 		return dst[:0], encoding.ErrEncodeInvalidInputSize(e, "FIXED_LEN_BYTE_ARRAY", len(src))
 	}
-	return e.encode(dst[:0], len(src)/size, func(i int) []byte {
-		j := (i + 0) * size
-		k := (i + 1) * size
-		return src[j:k:k]
-	})
-}
 
-func (e *ByteArrayEncoding) encode(dst []byte, numValues int, valueAt func(int) []byte) ([]byte, error) {
 	prefix := getInt32Buffer()
 	defer putInt32Buffer(prefix)
 
 	length := getInt32Buffer()
 	defer putInt32Buffer(length)
 
-	var lastValue []byte
-	for i := 0; i < numValues; i++ {
-		value := valueAt(i)
-		if len(value) > math.MaxInt32 {
-			return dst, encoding.Errorf(e, "byte array of length %d is too large to be encoded", len(value))
-		}
-		n := prefixLength(lastValue, value)
-		prefix.values = append(prefix.values, int32(n))
-		length.values = append(length.values, int32(len(value)-n))
-		lastValue = value
+	totalSize := 0
+	lastValue := ([]byte)(nil)
+
+	for i := size; i <= len(src); i += size {
+		v := src[i-size : i : i]
+		p := linearSearchPrefixLength(lastValue, v)
+		n := size - p
+		prefix.values = append(prefix.values, int32(p))
+		length.values = append(length.values, int32(n))
+		lastValue = v
+		totalSize += n
 	}
 
-	var binpack BinaryPackedEncoding
-	var err error
-	dst, err = binpack.encodeInt32(dst, prefix.values)
-	if err != nil {
-		return dst, err
-	}
-	dst, err = binpack.encodeInt32(dst, length.values)
-	if err != nil {
-		return dst, err
-	}
-	for i, p := range prefix.values {
-		dst = append(dst, valueAt(i)[p:]...)
+	dst = dst[:0]
+	dst = encodeInt32(dst, prefix.values)
+	dst = encodeInt32(dst, length.values)
+	dst = resize(dst, len(dst)+totalSize)
+
+	b := dst[len(dst)-totalSize:]
+	i := 0
+	j := 0
+
+	for _, p := range prefix.values {
+		j += copy(b[j:], src[i+int(p):i+size])
+		i += size
 	}
+
 	return dst, nil
 }
 
@@ -187,8 +222,11 @@ func (e *ByteArrayEncoding) decode(src []byte, observe func(prefix, suffix []byt
 	return nil
 }
 
-func prefixLength(base, data []byte) int {
-	return binarySearchPrefixLength(len(base)/2, base, data)
+func linearSearchPrefixLength(base, data []byte) (n int) {
+	for n < len(base) && n < len(data) && base[n] == data[n] {
+		n++
+	}
+	return n
 }
 
 func binarySearchPrefixLength(max int, base, data []byte) int {
diff --git a/encoding/delta/delta.go b/encoding/delta/delta.go
index e2168a1..25f235b 100644
--- a/encoding/delta/delta.go
+++ b/encoding/delta/delta.go
@@ -2,10 +2,7 @@ package delta
 
 import (
 	"sync"
-)
-
-const (
-	defaultBufferSize = 4096
+	"unsafe"
 )
 
 type int32Buffer struct {
@@ -36,3 +33,11 @@ func getInt32Buffer() *int32Buffer {
 func putInt32Buffer(b *int32Buffer) {
 	int32BufferPool.Put(b)
 }
+
+func bytesToInt32(b []byte) []int32 {
+	return unsafe.Slice(*(**int32)(unsafe.Pointer(&b)), len(b)/4)
+}
+
+func bytesToInt64(b []byte) []int64 {
+	return unsafe.Slice(*(**int64)(unsafe.Pointer(&b)), len(b)/8)
+}
diff --git a/encoding/delta/length_byte_array.go b/encoding/delta/length_byte_array.go
index 72380f4..bddd3ec 100644
--- a/encoding/delta/length_byte_array.go
+++ b/encoding/delta/length_byte_array.go
@@ -1,9 +1,6 @@
 package delta
 
 import (
-	"fmt"
-	"math"
-
 	"github.com/segmentio/parquet-go/encoding"
 	"github.com/segmentio/parquet-go/encoding/plain"
 	"github.com/segmentio/parquet-go/format"
@@ -22,33 +19,51 @@ func (e *LengthByteArrayEncoding) Encoding() format.Encoding {
 }
 
 func (e *LengthByteArrayEncoding) EncodeByteArray(dst, src []byte) ([]byte, error) {
-	return e.encodeByteArray(dst[:0], src)
+	dst, err := e.encodeByteArray(dst[:0], src)
+	if err != nil {
+		err = encoding.Error(e, err)
+	}
+	return dst, err
 }
 
 func (e *LengthByteArrayEncoding) encodeByteArray(dst, src []byte) ([]byte, error) {
-	b := getInt32Buffer()
-	defer putInt32Buffer(b)
+	length := getInt32Buffer()
+	defer putInt32Buffer(length)
+
+	totalSize := 0
 
-	err := plain.RangeByteArrays(src, func(value []byte) error {
-		if len(value) > math.MaxInt32 {
-			return fmt.Errorf("byte array of length %d is too large to be encoded", len(value))
+	for i := 0; i < len(src); {
+		r := len(src) - i
+		if r < plain.ByteArrayLengthSize {
+			return dst, plain.ErrTooShort(r)
 		}
-		b.values = append(b.values, int32(len(value)))
-		return nil
-	})
-	if err != nil {
-		return dst, encoding.Error(e, err)
+		n := plain.ByteArrayLength(src[i:])
+		i += plain.ByteArrayLengthSize
+		r -= plain.ByteArrayLengthSize
+		if n > r {
+			return dst, plain.ErrTooShort(n)
+		}
+		if n > plain.MaxByteArrayLength {
+			return dst, plain.ErrTooLarge(n)
+		}
+		length.values = append(length.values, int32(n))
+		totalSize += n
+		i += n
 	}
 
-	binpack := BinaryPackedEncoding{}
-	dst, err = binpack.encodeInt32(dst, b.values)
-	if err != nil {
-		return dst, encoding.Error(e, err)
+	dst = encodeInt32(dst, length.values)
+	dst = resize(dst, len(dst)+totalSize)
+
+	b := dst[len(dst)-totalSize:]
+	i := plain.ByteArrayLengthSize
+	j := 0
+
+	for _, n := range length.values {
+		j += copy(b[j:], src[i:i+int(n)])
+		i += plain.ByteArrayLengthSize
+		i += int(n)
 	}
-	plain.RangeByteArrays(src, func(value []byte) error {
-		dst = append(dst, value...)
-		return nil
-	})
+
 	return dst, nil
 }
 
diff --git a/encoding/plain/plain.go b/encoding/plain/plain.go
index 5a29fc6..e628e83 100644
--- a/encoding/plain/plain.go
+++ b/encoding/plain/plain.go
@@ -16,6 +16,7 @@ import (
 
 const (
 	ByteArrayLengthSize = 4
+	MaxByteArrayLength  = math.MaxInt32
 )
 
 type Encoding struct {
@@ -70,7 +71,7 @@ func (e *Encoding) EncodeDouble(dst, src []byte) ([]byte, error) {
 }
 
 func (e *Encoding) EncodeByteArray(dst []byte, src []byte) ([]byte, error) {
-	if err := RangeByteArrays(src, func([]byte) error { return nil }); err != nil {
+	if err := ValidateByteArrays(src); err != nil {
 		return dst[:0], encoding.Error(e, err)
 	}
 	return append(dst[:0], src...), nil
@@ -123,7 +124,7 @@ func (e *Encoding) DecodeDouble(dst, src []byte) ([]byte, error) {
 }
 
 func (e *Encoding) DecodeByteArray(dst, src []byte) ([]byte, error) {
-	if err := RangeByteArrays(src, func([]byte) error { return nil }); err != nil {
+	if err := ValidateByteArrays(src); err != nil {
 		return dst[:0], encoding.Error(e, err)
 	}
 	return append(dst[:0], src...), nil
@@ -231,6 +232,25 @@ func PutByteArrayLength(b []byte, n int) {
 	binary.LittleEndian.PutUint32(b, uint32(n))
 }
 
+type status int
+
+const (
+	ok status = iota
+	errTooShort
+	errTooLarge
+)
+
+func ValidateByteArrays(b []byte) error {
+	switch validateByteArrays(b) {
+	case errTooShort:
+		return ErrTooShort(len(b))
+	case errTooLarge:
+		return ErrTooLarge(len(b))
+	default: // ok
+		return nil
+	}
+}
+
 func RangeByteArrays(b []byte, do func([]byte) error) (err error) {
 	for len(b) > 0 {
 		var v []byte
@@ -245,12 +265,24 @@ func RangeByteArrays(b []byte, do func([]byte) error) (err error) {
 }
 
 func NextByteArray(b []byte) (v, r []byte, err error) {
-	if len(b) < 4 {
-		return nil, b, fmt.Errorf("input of length %d is too short to contain a PLAIN encoded byte array: %w", len(b), io.ErrUnexpectedEOF)
+	if len(b) < ByteArrayLengthSize {
+		return nil, b, ErrTooShort(len(b))
+	}
+	n := ByteArrayLength(b)
+	if n > (len(b) - ByteArrayLengthSize) {
+		return nil, b, ErrTooShort(len(b))
 	}
-	n := 4 + int(binary.LittleEndian.Uint32(b))
-	if n > len(b) {
-		return nil, b, fmt.Errorf("input of length %d is too short to contain a PLAIN encoded byte array of length %d: %w", len(b)-4, n-4, io.ErrUnexpectedEOF)
+	if n > MaxByteArrayLength {
+		return nil, b, ErrTooLarge(n)
 	}
-	return b[4:n:n], b[n:len(b):len(b)], nil
+	n += ByteArrayLengthSize
+	return b[ByteArrayLengthSize:n:n], b[n:len(b):len(b)], nil
+}
+
+func ErrTooShort(length int) error {
+	return fmt.Errorf("input of length %d is too short to contain a PLAIN encoded byte array value: %w", length, io.ErrUnexpectedEOF)
+}
+
+func ErrTooLarge(length int) error {
+	return fmt.Errorf("byte array of length %d is too large to be encoded", length)
 }
diff --git a/encoding/plain/plain_amd64.go b/encoding/plain/plain_amd64.go
new file mode 100644
index 0000000..96efec1
--- /dev/null
+++ b/encoding/plain/plain_amd64.go
@@ -0,0 +1,6 @@
+//go:build !purego
+
+package plain
+
+//go:noescape
+func validateByteArrays([]byte) status
diff --git a/encoding/plain/plain_amd64.s b/encoding/plain/plain_amd64.s
new file mode 100644
index 0000000..b8a6067
--- /dev/null
+++ b/encoding/plain/plain_amd64.s
@@ -0,0 +1,46 @@
+//go:build !purego
+
+#include "textflag.h"
+
+// validateByteArrays 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
+    MOVQ arg+0(FP), AX
+    MOVQ arg+8(FP), BX
+
+    CMPQ BX, $0
+    JE done
+
+    XORQ SI, SI
+loop:
+    MOVQ BX, CX
+    SUBQ SI, CX
+    CMPQ CX, $4
+    JB errTooShort
+
+    MOVLQZX (AX)(SI*1), DX
+    ADDQ $4, SI
+    SUBQ $4, CX
+    CMPQ DX, CX
+    JA errTooShort
+    CMPQ DX, $0x7FFFFFFF
+    JA errTooLarge
+
+    ADDQ DX, SI
+    CMPQ SI, BX
+    JNE loop
+done:
+    MOVQ $0, CX
+    MOVQ CX, ret+24(FP)
+    RET
+errTooShort:
+    MOVQ $1, CX
+    MOVQ CX, ret+24(FP)
+    RET
+errTooLarge:
+    MOVQ $2, CX
+    MOVQ CX, ret+32(FP)
+    RET
diff --git a/encoding/plain/plain_purego.go b/encoding/plain/plain_purego.go
new file mode 100644
index 0000000..4b4e7ad
--- /dev/null
+++ b/encoding/plain/plain_purego.go
@@ -0,0 +1,23 @@
+//go:build purego || !amd64
+
+package plain
+
+func validateByteArrays(b []byte) status {
+	for i := 0; i < len(b); {
+		r := len(b) - i
+		if r < ByteArrayLengthSize {
+			return errTooShort
+		}
+		n := ByteArrayLength(b[i:])
+		i += ByteArrayLengthSize
+		r -= ByteArrayLengthSize
+		if n > r {
+			return errTooShort
+		}
+		if n > MaxByteArrayLength {
+			return errTooLarge
+		}
+		i += n
+	}
+	return ok
+}
diff --git a/go.mod b/go.mod
index c149187..690e0d3 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/andybalholm/brotli v1.0.3
 	github.com/google/uuid v1.3.0
 	github.com/hexops/gotextdiff v1.0.3
-	github.com/klauspost/compress v1.15.2
+	github.com/klauspost/compress v1.15.5
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/pierrec/lz4/v4 v4.1.9
 	github.com/segmentio/encoding v0.3.5
diff --git a/go.sum b/go.sum
index e5d4daa..dd857c4 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
-github.com/klauspost/compress v1.15.2 h1:3WH+AG7s2+T8o3nrM/8u2rdqUEcQhmga7smjrT41nAw=
-github.com/klauspost/compress v1.15.2/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
+github.com/klauspost/compress v1.15.5 h1:qyCLMz2JCrKADihKOh9FxnW3houKeNsp2h5OEz0QSEA=
+github.com/klauspost/compress v1.15.5/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
 github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
diff --git a/internal/quick/quick.go b/internal/quick/quick.go
new file mode 100644
index 0000000..bce7ef0
--- /dev/null
+++ b/internal/quick/quick.go
@@ -0,0 +1,174 @@
+package quick
+
+import (
+	"fmt"
+	"math/rand"
+	"reflect"
+	"strings"
+)
+
+var DefaultConfig = Config{
+	Sizes: []int{
+		0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+		10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
+		20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
+		30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
+		99, 100, 101,
+		127, 128, 129,
+		255, 256, 257,
+		1000, 1023, 1024, 1025,
+		2000, 2095, 2048, 2049,
+		4000, 4095, 4096, 4097,
+	},
+	Seed: 0,
+}
+
+// Check is inspired by the standard quick.Check package, but enhances the
+// API and tests arrays of larger sizes than the maximum of 50 hardcoded in
+// testing/quick.
+func Check(f interface{}) error {
+	return DefaultConfig.Check(f)
+}
+
+type Config struct {
+	Sizes []int
+	Seed  int64
+}
+
+func (c *Config) Check(f interface{}) error {
+	v := reflect.ValueOf(f)
+	r := rand.New(rand.NewSource(c.Seed))
+	t := v.Type().In(0)
+
+	makeValue := MakeValueFuncOf(t.Elem())
+	makeArray := func(n int) reflect.Value {
+		array := reflect.MakeSlice(t, n, n)
+		for i := 0; i < n; i++ {
+			makeValue(array.Index(i), r)
+		}
+		return array
+	}
+
+	if makeArray == nil {
+		panic("cannot run quick check on function with input of type " + v.Type().In(0).String())
+	}
+
+	for _, n := range c.Sizes {
+		for i := 0; i < 3; i++ {
+			in := makeArray(n)
+			ok := v.Call([]reflect.Value{in})
+			if !ok[0].Bool() {
+				return fmt.Errorf("test #%d: failed on input of size %d: %#v\n", i+1, n, in.Interface())
+			}
+		}
+	}
+	return nil
+
+}
+
+type MakeValueFunc func(reflect.Value, *rand.Rand)
+
+func MakeValueFuncOf(t reflect.Type) MakeValueFunc {
+	switch t.Kind() {
+	case reflect.Bool:
+		return func(v reflect.Value, r *rand.Rand) {
+			v.SetBool((r.Int() % 2) != 0)
+		}
+
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return func(v reflect.Value, r *rand.Rand) {
+			v.SetInt(r.Int63())
+		}
+
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return func(v reflect.Value, r *rand.Rand) {
+			v.SetUint(r.Uint64())
+		}
+
+	case reflect.Float32, reflect.Float64:
+		return func(v reflect.Value, r *rand.Rand) {
+			v.SetFloat(r.Float64())
+		}
+
+	case reflect.String:
+		return func(v reflect.Value, r *rand.Rand) {
+			const characters = "1234567890qwertyuiopasdfghjklzxcvbnm"
+			s := new(strings.Builder)
+			n := r.Intn(10)
+			for i := 0; i < n; i++ {
+				s.WriteByte(characters[i])
+			}
+			v.SetString(s.String())
+		}
+
+	case reflect.Array:
+		makeElem := MakeValueFuncOf(t.Elem())
+		return func(v reflect.Value, r *rand.Rand) {
+			for i, n := 0, v.Len(); i < n; i++ {
+				makeElem(v.Index(i), r)
+			}
+		}
+
+	case reflect.Slice:
+		switch e := t.Elem(); e.Kind() {
+		case reflect.Uint8:
+			return func(v reflect.Value, r *rand.Rand) {
+				b := make([]byte, r.Intn(50))
+				r.Read(b)
+				v.SetBytes(b)
+			}
+		default:
+			makeElem := MakeValueFuncOf(t.Elem())
+			return func(v reflect.Value, r *rand.Rand) {
+				n := r.Intn(10)
+				s := reflect.MakeSlice(t, n, n)
+				for i := 0; i < n; i++ {
+					makeElem(s.Index(i), r)
+				}
+				v.Set(s)
+			}
+		}
+
+	case reflect.Map:
+		makeKey := MakeValueFuncOf(t.Key())
+		makeElem := MakeValueFuncOf(t.Elem())
+		return func(v reflect.Value, r *rand.Rand) {
+			m := reflect.MakeMap(t)
+			n := r.Intn(10)
+			k := reflect.New(t.Key()).Elem()
+			e := reflect.New(t.Elem()).Elem()
+			for i := 0; i < n; i++ {
+				makeKey(k, r)
+				makeElem(e, r)
+				m.SetMapIndex(k, e)
+			}
+			v.Set(m)
+		}
+
+	case reflect.Struct:
+		fields := make([]reflect.StructField, 0, t.NumField())
+		makeValues := make([]MakeValueFunc, 0, cap(fields))
+		for i, n := 0, cap(fields); i < n; i++ {
+			if f := t.Field(i); f.PkgPath == "" { // skip unexported fields
+				fields = append(fields, f)
+				makeValues = append(makeValues, MakeValueFuncOf(f.Type))
+			}
+		}
+		return func(v reflect.Value, r *rand.Rand) {
+			for i := range fields {
+				makeValues[i](v.FieldByIndex(fields[i].Index), r)
+			}
+		}
+
+	case reflect.Ptr:
+		t = t.Elem()
+		makeValue := MakeValueFuncOf(t)
+		return func(v reflect.Value, r *rand.Rand) {
+			v.Set(reflect.New(t))
+			makeValue(v.Elem(), r)
+		}
+
+	default:
+		panic("quick.Check does not support test values of type " + t.String())
+	}
+}
diff --git a/row.go b/row.go
index b15ccb7..d135fb8 100644
--- a/row.go
+++ b/row.go
@@ -21,6 +21,19 @@ const (
 // position in the row.
 type Row []Value
 
+// Clone creates a copy of the row which shares no pointers.
+//
+// This method is useful to capture rows after a call to RowReader.ReadRows when
+// values need to be retained before the next call to ReadRows or after the lifespan
+// of the reader.
+func (row Row) Clone() Row {
+	clone := make(Row, len(row))
+	for i := range row {
+		clone[i] = row[i].Clone()
+	}
+	return clone
+}
+
 // Equal returns true if row and other contain the same sequence of values.
 func (row Row) Equal(other Row) bool {
 	if len(row) != len(other) {
diff --git a/schema.go b/schema.go
index e985f7b..cd587fb 100644
--- a/schema.go
+++ b/schema.go
@@ -56,6 +56,7 @@ type Schema struct {
 //	decimal   | for int32, int64 and [n]byte types, use the parquet DECIMAL logical type
 //	date      | for int32 types use the DATE logical type
 //	timestamp | for int64 types use the TIMESTAMP logical type with millisecond precision
+//	split     | for float32/float64, use the BYTE_STREAM_SPLIT encoding
 //
 // The date logical type is an int32 value of the number of days since the unix epoch
 //
@@ -69,6 +70,10 @@ type Schema struct {
 // Invalid combination of struct tags and Go types, or repeating options will
 // cause the function to panic.
 //
+// As a special case, if the field tag is "-", the field is omitted from the schema
+// and the data will not be written into the parquet file(s).
+// Note that a field with name "-" can still be generated using the tag "-,".
+//
 // The schema name is the Go type name of the value.
 func SchemaOf(model interface{}) *Schema {
 	return schemaOf(dereference(reflect.TypeOf(model)))
@@ -314,10 +319,17 @@ func structFieldsOf(t reflect.Type) []reflect.StructField {
 
 func appendStructFields(t reflect.Type, fields []reflect.StructField, index []int, offset uintptr) []reflect.StructField {
 	for i, n := 0, t.NumField(); i < n; i++ {
+		f := t.Field(i)
+		if tag := f.Tag.Get("parquet"); tag != "" {
+			name, _ := split(tag)
+			if tag != "-," && name == "-" {
+				continue
+			}
+		}
+
 		fieldIndex := index[:len(index):len(index)]
 		fieldIndex = append(fieldIndex, i)
 
-		f := t.Field(i)
 		f.Offset += offset
 
 		if f.Anonymous {
@@ -391,7 +403,6 @@ func (f *structField) Value(base reflect.Value) reflect.Value {
 			return fieldByIndex(base, f.index)
 		}
 	}
-	return reflect.Value{}
 }
 
 func structFieldString(f reflect.StructField) string {
@@ -505,6 +516,14 @@ func makeStructField(f reflect.StructField) structField {
 				throwInvalidFieldTag(f, option)
 			}
 
+		case "split":
+			switch f.Type.Kind() {
+			case reflect.Float32, reflect.Float64:
+				setEncoding(&ByteStreamSplit)
+			default:
+				throwInvalidFieldTag(f, option)
+			}
+
 		case "list":
 			switch f.Type.Kind() {
 			case reflect.Slice:


Test patch:
diff --git a/bloom_test.go b/bloom_test.go
index 8ef0aa8..81f4c58 100644
--- a/bloom_test.go
+++ b/bloom_test.go
@@ -3,12 +3,12 @@ package parquet
 import (
 	"math/rand"
 	"testing"
-	"testing/quick"
 
 	"github.com/segmentio/parquet-go/bloom"
 	"github.com/segmentio/parquet-go/deprecated"
 	"github.com/segmentio/parquet-go/encoding/plain"
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func TestSplitBlockFilter(t *testing.T) {
@@ -145,7 +145,7 @@ func TestSplitBlockFilter(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.scenario, func(t *testing.T) {
-			if err := quick.Check(test.function, nil); err != nil {
+			if err := quick.Check(test.function); err != nil {
 				t.Error(err)
 			}
 		})
diff --git a/buffer_go18_test.go b/buffer_go18_test.go
index 3d83e98..7848a52 100644
--- a/buffer_go18_test.go
+++ b/buffer_go18_test.go
@@ -9,7 +9,6 @@ import (
 	"math/rand"
 	"reflect"
 	"testing"
-	"testing/quick"
 
 	"github.com/segmentio/parquet-go"
 )
@@ -36,13 +35,13 @@ func TestGenericBuffer(t *testing.T) {
 	testGenericBuffer[nestedListColumn1](t)
 	testGenericBuffer[nestedListColumn](t)
 	testGenericBuffer[*contact](t)
+	testGenericBuffer[paddedBooleanColumn](t)
 }
 
 func testGenericBuffer[Row any](t *testing.T) {
-	var prng = rand.New(rand.NewSource(1))
 	var model Row
 	t.Run(reflect.TypeOf(model).Name(), func(t *testing.T) {
-		f := func(rows []Row) bool {
+		err := quickCheck(func(rows []Row) bool {
 			if len(rows) == 0 {
 				return true // TODO: fix support for parquet files with zero rows
 			}
@@ -51,8 +50,8 @@ func testGenericBuffer[Row any](t *testing.T) {
 				return false
 			}
 			return true
-		}
-		if err := quick.Check(f, &quick.Config{Rand: prng}); err != nil {
+		})
+		if err != nil {
 			t.Error(err)
 		}
 	})
@@ -110,6 +109,7 @@ func BenchmarkGenericBuffer(b *testing.B) {
 	benchmarkGenericBuffer[mapColumn](b)
 	benchmarkGenericBuffer[decimalColumn](b)
 	benchmarkGenericBuffer[contact](b)
+	benchmarkGenericBuffer[paddedBooleanColumn](b)
 }
 
 func benchmarkGenericBuffer[Row generator[Row]](b *testing.B) {
diff --git a/buffer_test.go b/buffer_test.go
index 77fa7e5..e1c4d62 100644
--- a/buffer_test.go
+++ b/buffer_test.go
@@ -10,7 +10,6 @@ import (
 	"sort"
 	"strconv"
 	"testing"
-	"testing/quick"
 
 	"github.com/segmentio/parquet-go"
 	"github.com/segmentio/parquet-go/encoding"
@@ -413,7 +412,7 @@ func TestBufferGenerateBloomFilters(t *testing.T) {
 		return true
 	}
 
-	if err := quick.Check(f, nil); err != nil {
+	if err := quickCheck(f); err != nil {
 		t.Error(err)
 	}
 }
diff --git a/column_test.go b/column_test.go
index b54db61..e2d7a7d 100644
--- a/column_test.go
+++ b/column_test.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"math/rand"
 	"testing"
-	"testing/quick"
 
 	"github.com/google/uuid"
 	"github.com/segmentio/parquet-go"
@@ -102,9 +101,7 @@ func TestColumnPageIndex(t *testing.T) {
 				},
 			} {
 				t.Run(test.scenario, func(t *testing.T) {
-					if err := quick.Check(test.function(t), &quick.Config{
-						Rand: rand.New(rand.NewSource(0)),
-					}); err != nil {
+					if err := quickCheck(test.function(t)); err != nil {
 						t.Error(err)
 					}
 				})
diff --git a/encoding/delta/binary_packed_amd64_test.go b/encoding/delta/binary_packed_amd64_test.go
new file mode 100644
index 0000000..d088531
--- /dev/null
+++ b/encoding/delta/binary_packed_amd64_test.go
@@ -0,0 +1,131 @@
+//go:build amd64 && !purego
+
+package delta
+
+import (
+	"testing"
+
+	"golang.org/x/sys/cpu"
+)
+
+func requireAVX2(t testing.TB) {
+	if !cpu.X86.HasAVX2 {
+		t.Skip("CPU does not support AVX2")
+	}
+}
+
+func TestBlockDeltaInt32AVX2(t *testing.T) {
+	requireAVX2(t)
+	testBlockDeltaInt32(t, blockDeltaInt32AVX2)
+}
+
+func TestBlockMinInt32AVX2(t *testing.T) {
+	requireAVX2(t)
+	testBlockMinInt32(t, blockMinInt32AVX2)
+}
+
+func TestBlockSubInt32AVX2(t *testing.T) {
+	requireAVX2(t)
+	testBlockSubInt32(t, blockSubInt32AVX2)
+}
+
+func TestBlockBitWidthsInt32AVX2(t *testing.T) {
+	requireAVX2(t)
+	testBlockBitWidthsInt32(t, blockBitWidthsInt32AVX2)
+}
+
+func TestMiniBlockPackInt32AVX2(t *testing.T) {
+	requireAVX2(t)
+	testMiniBlockPackInt32(t,
+		func(dst []byte, src *[miniBlockSize]int32, bitWidth uint) {
+			miniBlockPackInt32AVX2(&dst[0], src, bitWidth)
+		},
+	)
+}
+
+func BenchmarkBlockDeltaInt32AVX2(b *testing.B) {
+	requireAVX2(b)
+	benchmarkBlockDeltaInt32(b, blockDeltaInt32AVX2)
+}
+
+func BenchmarkBlockMinInt32AVX2(b *testing.B) {
+	requireAVX2(b)
+	benchmarkBlockMinInt32(b, blockMinInt32AVX2)
+}
+
+func BenchmarkBlockSubInt32AVX2(b *testing.B) {
+	requireAVX2(b)
+	benchmarkBlockSubInt32(b, blockSubInt32AVX2)
+}
+
+func BenchmarkBlockBitWidthsInt32AVX2(b *testing.B) {
+	requireAVX2(b)
+	benchmarkBlockBitWidthsInt32(b, blockBitWidthsInt32AVX2)
+}
+
+func BenchmarkMiniBlockPackInt32AVX2(b *testing.B) {
+	requireAVX2(b)
+	benchmarkMiniBlockPackInt32(b,
+		func(dst []byte, src *[miniBlockSize]int32, bitWidth uint) {
+			miniBlockPackInt32AVX2(&dst[0], src, bitWidth)
+		},
+	)
+}
+
+func TestBlockDeltaInt64AVX2(t *testing.T) {
+	requireAVX2(t)
+	testBlockDeltaInt64(t, blockDeltaInt64AVX2)
+}
+
+func TestBlockMinInt64AVX2(t *testing.T) {
+	requireAVX2(t)
+	testBlockMinInt64(t, blockMinInt64AVX2)
+}
+
+func TestBlockSubInt64AVX2(t *testing.T) {
+	requireAVX2(t)
+	testBlockSubInt64(t, blockSubInt64AVX2)
+}
+
+func TestBlockBitWidthsInt64AVX2(t *testing.T) {
+	requireAVX2(t)
+	testBlockBitWidthsInt64(t, blockBitWidthsInt64AVX2)
+}
+
+func TestMiniBlockPackInt64AVX2(t *testing.T) {
+	requireAVX2(t)
+	testMiniBlockPackInt64(t,
+		func(dst []byte, src *[miniBlockSize]int64, bitWidth uint) {
+			miniBlockPackInt64AVX2(&dst[0], src, bitWidth)
+		},
+	)
+}
+
+func BenchmarkBlockDeltaInt64AVX2(b *testing.B) {
+	requireAVX2(b)
+	benchmarkBlockDeltaInt64(b, blockDeltaInt64AVX2)
+}
+
+func BenchmarkBlockMinInt64AVX2(b *testing.B) {
+	requireAVX2(b)
+	benchmarkBlockMinInt64(b, blockMinInt64AVX2)
+}
+
+func BenchmarkBlockSubInt64AVX2(b *testing.B) {
+	requireAVX2(b)
+	benchmarkBlockSubInt64(b, blockSubInt64AVX2)
+}
+
+func BenchmarkBlockBitWidthsInt64AVX2(b *testing.B) {
+	requireAVX2(b)
+	benchmarkBlockBitWidthsInt64(b, blockBitWidthsInt64AVX2)
+}
+
+func BenchmarkMiniBlockPackInt64AVX2(b *testing.B) {
+	requireAVX2(b)
+	benchmarkMiniBlockPackInt64(b,
+		func(dst []byte, src *[miniBlockSize]int64, bitWidth uint) {
+			miniBlockPackInt64AVX2(&dst[0], src, bitWidth)
+		},
+	)
+}
diff --git a/encoding/delta/binary_packed_purego_test.go b/encoding/delta/binary_packed_purego_test.go
new file mode 100644
index 0000000..80e75c4
--- /dev/null
+++ b/encoding/delta/binary_packed_purego_test.go
@@ -0,0 +1,388 @@
+package delta
+
+import (
+	"bytes"
+	"fmt"
+	"math/bits"
+	"testing"
+)
+
+func maxLen32(miniBlock []int32) (maxLen int) {
+	for _, v := range miniBlock {
+		if n := bits.Len32(uint32(v)); n > maxLen {
+			maxLen = n
+		}
+	}
+	return maxLen
+}
+
+func maxLen64(miniBlock []int64) (maxLen int) {
+	for _, v := range miniBlock {
+		if n := bits.Len64(uint64(v)); n > maxLen {
+			maxLen = n
+		}
+	}
+	return maxLen
+}
+
+func TestBlockDeltaInt32(t *testing.T) {
+	testBlockDeltaInt32(t, blockDeltaInt32)
+}
+
+func testBlockDeltaInt32(t *testing.T, f func(*[blockSize]int32, int32) int32) {
+	t.Helper()
+	block := [blockSize]int32{}
+	for i := range block {
+		block[i] = int32(2 * (i + 1))
+	}
+	lastValue := f(&block, 0)
+	if lastValue != 2*blockSize {
+		t.Errorf("wrong last block value: want=%d got=%d", 2*blockSize, lastValue)
+	}
+	for i := range block {
+		j := int32(2 * (i + 0))
+		k := int32(2 * (i + 1))
+		if block[i] != (k - j) {
+			t.Errorf("wrong block delta at index %d: want=%d got=%d", i, k-j, block[i])
+		}
+	}
+}
+
+func TestBlockMinInt32(t *testing.T) {
+	testBlockMinInt32(t, blockMinInt32)
+}
+
+func testBlockMinInt32(t *testing.T, f func(*[blockSize]int32) int32) {
+	t.Helper()
+	block := [blockSize]int32{}
+	for i := range block {
+		block[i] = blockSize - int32(i)
+	}
+	if min := f(&block); min != 1 {
+		t.Errorf("wrong min block value: want=1 got=%d", min)
+	}
+}
+
+func TestBlockSubInt32(t *testing.T) {
+	testBlockSubInt32(t, blockSubInt32)
+}
+
+func testBlockSubInt32(t *testing.T, f func(*[blockSize]int32, int32)) {
+	t.Helper()
+	block := [blockSize]int32{}
+	for i := range block {
+		block[i] = int32(i)
+	}
+	f(&block, 1)
+	for i := range block {
+		if block[i] != int32(i-1) {
+			t.Errorf("wrong block value at index %d: want=%d got=%d", i, i-1, block[i])
+		}
+	}
+}
+
+func TestBlockBitWidthsInt32(t *testing.T) {
+	testBlockBitWidthsInt32(t, blockBitWidthsInt32)
+}
+
+func testBlockBitWidthsInt32(t *testing.T, f func(*[numMiniBlocks]byte, *[blockSize]int32)) {
+	t.Helper()
+	bitWidths := [numMiniBlocks]byte{}
+	block := [blockSize]int32{}
+	for i := range block {
+		block[i] = int32(i)
+	}
+	f(&bitWidths, &block)
+
+	want := [numMiniBlocks]byte{}
+	for i := range want {
+		j := (i + 0) * miniBlockSize
+		k := (i + 1) * miniBlockSize
+		want[i] = byte(maxLen32(block[j:k]))
+	}
+
+	if bitWidths != want {
+		t.Errorf("wrong bit widths: want=%d got=%d", want, bitWidths)
+	}
+}
+
+func TestMiniBlockPackInt32(t *testing.T) {
+	testMiniBlockPackInt32(t, miniBlockPackInt32)
+}
+
+func testMiniBlockPackInt32(t *testing.T, f func([]byte, *[miniBlockSize]int32, uint)) {
+	t.Helper()
+	for bitWidth := uint(1); bitWidth <= 32; bitWidth++ {
+		t.Run(fmt.Sprintf("bitWidth=%d", bitWidth), func(t *testing.T) {
+			got := [4*miniBlockSize + 32]byte{}
+			src := [miniBlockSize]int32{}
+			for i := range src {
+				src[i] = int32(i) & int32((1<<bitWidth)-1)
+			}
+
+			want := [4*miniBlockSize + 32]byte{}
+			bitOffset := uint(0)
+
+			for _, bits := range src {
+				for b := uint(0); b < bitWidth; b++ {
+					x := bitOffset / 8
+					y := bitOffset % 8
+					want[x] |= byte(((bits >> b) & 1) << y)
+					bitOffset++
+				}
+			}
+
+			f(got[:], &src, bitWidth)
+			n := (miniBlockSize * bitWidth) / 8
+
+			if !bytes.Equal(want[:n], got[:n]) {
+				t.Errorf("output mismatch: want=%08x got=%08x", want[:n], got[:n])
+			}
+		})
+	}
+}
+
+func BenchmarkBlockDeltaInt32(b *testing.B) {
+	benchmarkBlockDeltaInt32(b, blockDeltaInt32)
+}
+
+func benchmarkBlockDeltaInt32(b *testing.B, f func(*[blockSize]int32, int32) int32) {
+	b.SetBytes(4 * blockSize)
+	block := [blockSize]int32{}
+	for i := 0; i < b.N; i++ {
+		_ = f(&block, 0)
+	}
+}
+
+func BenchmarkBlockMinInt32(b *testing.B) {
+	benchmarkBlockMinInt32(b, blockMinInt32)
+}
+
+func benchmarkBlockMinInt32(b *testing.B, f func(*[blockSize]int32) int32) {
+	b.SetBytes(4 * blockSize)
+	block := [blockSize]int32{}
+	for i := 0; i < b.N; i++ {
+		_ = f(&block)
+	}
+}
+
+func BenchmarkBlockSubInt32(b *testing.B) {
+	benchmarkBlockSubInt32(b, blockSubInt32)
+}
+
+func benchmarkBlockSubInt32(b *testing.B, f func(*[blockSize]int32, int32)) {
+	b.SetBytes(4 * blockSize)
+	block := [blockSize]int32{}
+	for i := 0; i < b.N; i++ {
+		f(&block, 42)
+	}
+}
+
+func BenchmarkBlockBitWidthsInt32(b *testing.B) {
+	benchmarkBlockBitWidthsInt32(b, blockBitWidthsInt32)
+}
+
+func benchmarkBlockBitWidthsInt32(b *testing.B, f func(*[numMiniBlocks]byte, *[blockSize]int32)) {
+	b.SetBytes(4 * blockSize)
+	bitWidths := [numMiniBlocks]byte{}
+	block := [blockSize]int32{}
+	for i := 0; i < b.N; i++ {
+		f(&bitWidths, &block)
+	}
+}
+
+func BenchmarkMiniBlockPackInt32(b *testing.B) {
+	benchmarkMiniBlockPackInt32(b, miniBlockPackInt32)
+}
+
+func benchmarkMiniBlockPackInt32(b *testing.B, f func([]byte, *[miniBlockSize]int32, uint)) {
+	for bitWidth := uint(1); bitWidth <= 32; bitWidth++ {
+		b.Run(fmt.Sprintf("bitWidth=%d", bitWidth), func(b *testing.B) {
+			b.SetBytes(4 * miniBlockSize)
+			dst := [4*miniBlockSize + 32]byte{}
+			src := [miniBlockSize]int32{}
+			for i := 0; i < b.N; i++ {
+				f(dst[:], &src, bitWidth)
+			}
+		})
+	}
+}
+
+func TestBlockDeltaInt64(t *testing.T) {
+	testBlockDeltaInt64(t, blockDeltaInt64)
+}
+
+func testBlockDeltaInt64(t *testing.T, f func(*[blockSize]int64, int64) int64) {
+	t.Helper()
+	block := [blockSize]int64{}
+	for i := range block {
+		block[i] = int64(2 * (i + 1))
+	}
+	lastValue := f(&block, 0)
+	if lastValue != 2*blockSize {
+		t.Errorf("wrong last block value: want=%d got=%d", 2*blockSize, lastValue)
+	}
+	for i := range block {
+		j := int64(2 * (i + 0))
+		k := int64(2 * (i + 1))
+		if block[i] != (k - j) {
+			t.Errorf("wrong block delta at index %d: want=%d got=%d", i, k-j, block[i])
+		}
+	}
+}
+
+func TestBlockMinInt64(t *testing.T) {
+	testBlockMinInt64(t, blockMinInt64)
+}
+
+func testBlockMinInt64(t *testing.T, f func(*[blockSize]int64) int64) {
+	block := [blockSize]int64{}
+	for i := range block {
+		block[i] = blockSize - int64(i)
+	}
+	if min := f(&block); min != 1 {
+		t.Errorf("wrong min block value: want=1 got=%d", min)
+	}
+}
+
+func TestBlockSubInt64(t *testing.T) {
+	testBlockSubInt64(t, blockSubInt64)
+}
+
+func testBlockSubInt64(t *testing.T, f func(*[blockSize]int64, int64)) {
+	block := [blockSize]int64{}
+	for i := range block {
+		block[i] = int64(i)
+	}
+	f(&block, 1)
+	for i := range block {
+		if block[i] != int64(i-1) {
+			t.Errorf("wrong block value at index %d: want=%d got=%d", i, i-1, block[i])
+		}
+	}
+}
+
+func TestBlockBitWidthsInt64(t *testing.T) {
+	testBlockBitWidthsInt64(t, blockBitWidthsInt64)
+}
+
+func testBlockBitWidthsInt64(t *testing.T, f func(*[numMiniBlocks]byte, *[blockSize]int64)) {
+	bitWidths := [numMiniBlocks]byte{}
+	block := [blockSize]int64{}
+	for i := range block {
+		block[i] = int64(i)
+	}
+	f(&bitWidths, &block)
+
+	want := [numMiniBlocks]byte{}
+	for i := range want {
+		j := (i + 0) * miniBlockSize
+		k := (i + 1) * miniBlockSize
+		want[i] = byte(maxLen64(block[j:k]))
+	}
+
+	if bitWidths != want {
+		t.Errorf("wrong bit widths: want=%d got=%d", want, bitWidths)
+	}
+}
+
+func TestMiniBlockPackInt64(t *testing.T) {
+	testMiniBlockPackInt64(t, miniBlockPackInt64)
+}
+
+func testMiniBlockPackInt64(t *testing.T, f func([]byte, *[miniBlockSize]int64, uint)) {
+	for bitWidth := uint(1); bitWidth <= 64; bitWidth++ {
+		t.Run(fmt.Sprintf("bitWidth=%d", bitWidth), func(t *testing.T) {
+			got := [8*miniBlockSize + 64]byte{}
+			src := [miniBlockSize]int64{}
+			for i := range src {
+				src[i] = int64(i) & int64((1<<bitWidth)-1)
+			}
+
+			want := [8*miniBlockSize + 64]byte{}
+			bitOffset := uint(0)
+
+			for _, bits := range src {
+				for b := uint(0); b < bitWidth; b++ {
+					x := bitOffset / 8
+					y := bitOffset % 8
+					want[x] |= byte(((bits >> b) & 1) << y)
+					bitOffset++
+				}
+			}
+
+			f(got[:], &src, bitWidth)
+			n := (miniBlockSize * bitWidth) / 8
+
+			if !bytes.Equal(want[:n], got[:n]) {
+				t.Errorf("output mismatch: want=%08x got=%08x", want[:n], got[:n])
+			}
+		})
+	}
+}
+
+func BenchmarkBlockDeltaInt64(b *testing.B) {
+	benchmarkBlockDeltaInt64(b, blockDeltaInt64)
+}
+
+func benchmarkBlockDeltaInt64(b *testing.B, f func(*[blockSize]int64, int64) int64) {
+	b.SetBytes(8 * blockSize)
+	block := [blockSize]int64{}
+	for i := 0; i < b.N; i++ {
+		_ = f(&block, 0)
+	}
+}
+
+func BenchmarkBlockMinInt64(b *testing.B) {
+	benchmarkBlockMinInt64(b, blockMinInt64)
+}
+
+func benchmarkBlockMinInt64(b *testing.B, f func(*[blockSize]int64) int64) {
+	b.SetBytes(8 * blockSize)
+	block := [blockSize]int64{}
+	for i := 0; i < b.N; i++ {
+		_ = f(&block)
+	}
+}
+
+func BenchmarkBlockSubInt64(b *testing.B) {
+	benchmarkBlockSubInt64(b, blockSubInt64)
+}
+
+func benchmarkBlockSubInt64(b *testing.B, f func(*[blockSize]int64, int64)) {
+	b.SetBytes(8 * blockSize)
+	block := [blockSize]int64{}
+	for i := 0; i < b.N; i++ {
+		f(&block, 42)
+	}
+}
+
+func BenchmarkBlockBitWidthsInt64(b *testing.B) {
+	benchmarkBlockBitWidthsInt64(b, blockBitWidthsInt64)
+}
+
+func benchmarkBlockBitWidthsInt64(b *testing.B, f func(*[numMiniBlocks]byte, *[blockSize]int64)) {
+	b.SetBytes(8 * blockSize)
+	bitWidths := [numMiniBlocks]byte{}
+	block := [blockSize]int64{}
+	for i := 0; i < b.N; i++ {
+		f(&bitWidths, &block)
+	}
+}
+
+func BenchmarkMiniBlockPackInt64(b *testing.B) {
+	benchmarkMiniBlockPackInt64(b, miniBlockPackInt64)
+}
+
+func benchmarkMiniBlockPackInt64(b *testing.B, f func([]byte, *[miniBlockSize]int64, uint)) {
+	for bitWidth := uint(1); bitWidth <= 64; bitWidth++ {
+		b.Run(fmt.Sprintf("bitWidth=%d", bitWidth), func(b *testing.B) {
+			b.SetBytes(8 * miniBlockSize)
+			dst := [8*miniBlockSize + 64]byte{}
+			src := [miniBlockSize]int64{}
+			for i := 0; i < b.N; i++ {
+				f(dst[:], &src, bitWidth)
+			}
+		})
+	}
+}
diff --git a/encoding/delta/binary_packed_test.go b/encoding/delta/binary_packed_test.go
new file mode 100644
index 0000000..877b819
--- /dev/null
+++ b/encoding/delta/binary_packed_test.go
@@ -0,0 +1,42 @@
+//go:build amd64 && go1.18
+
+package delta_test
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/segmentio/parquet-go/encoding/delta"
+	"github.com/segmentio/parquet-go/encoding/test"
+)
+
+const (
+	encodeMinNumValues = 0
+	encodeMaxNumValues = 200
+)
+
+func TestEncodeInt32(t *testing.T) {
+	for bitWidth := uint(0); bitWidth <= 32; bitWidth++ {
+		t.Run(fmt.Sprintf("bitWidth=%d", bitWidth), func(t *testing.T) {
+			test.EncodeInt32(t,
+				new(delta.BinaryPackedEncoding),
+				encodeMinNumValues,
+				encodeMaxNumValues,
+				bitWidth,
+			)
+		})
+	}
+}
+
+func TestEncodeInt64(t *testing.T) {
+	for bitWidth := uint(0); bitWidth <= 64; bitWidth++ {
+		t.Run(fmt.Sprintf("bitWidth=%d", bitWidth), func(t *testing.T) {
+			test.EncodeInt64(t,
+				new(delta.BinaryPackedEncoding),
+				encodeMinNumValues,
+				encodeMaxNumValues,
+				bitWidth,
+			)
+		})
+	}
+}
diff --git a/encoding/delta/byte_array_test.go b/encoding/delta/byte_array_test.go
index 70bdb07..571a3da 100644
--- a/encoding/delta/byte_array_test.go
+++ b/encoding/delta/byte_array_test.go
@@ -5,7 +5,17 @@ import (
 	"testing"
 )
 
-func TestPrefixLength(t *testing.T) {
+func TestLinearSearchPrefixLength(t *testing.T) {
+	testSearchPrefixLength(t, linearSearchPrefixLength)
+}
+
+func TestBinarySearchPrefixLength(t *testing.T) {
+	testSearchPrefixLength(t, func(base, data []byte) int {
+		return binarySearchPrefixLength(len(base)/2, base, data)
+	})
+}
+
+func testSearchPrefixLength(t *testing.T, prefixLength func(base, data []byte) int) {
 	tests := []struct {
 		base string
 		data string
@@ -118,10 +128,20 @@ func TestPrefixLength(t *testing.T) {
 	}
 }
 
-func BenchmarkPrefixLength(b *testing.B) {
+func BenchmarkLinearSearchkPrefixLength(b *testing.B) {
+	benchmarkSearchPrefixLength(b, linearSearchPrefixLength)
+}
+
+func BenchmarkBinarySearchkPrefixLength(b *testing.B) {
+	benchmarkSearchPrefixLength(b, func(base, data []byte) int {
+		return binarySearchPrefixLength(len(base)/2, base, data)
+	})
+}
+
+func benchmarkSearchPrefixLength(b *testing.B, prefixLength func(base, data []byte) int) {
 	value := bytes.Repeat([]byte("0123456789"), 100)
 
 	for i := 0; i < b.N; i++ {
-		prefixLength(value, value)
+		_ = prefixLength(value, value)
 	}
 }
diff --git a/encoding/plain/plain_test.go b/encoding/plain/plain_test.go
index b6f09cf..fa610dd 100644
--- a/encoding/plain/plain_test.go
+++ b/encoding/plain/plain_test.go
@@ -32,3 +32,27 @@ func TestAppendBoolean(t *testing.T) {
 		t.Errorf("%08b\n", values)
 	}
 }
+
+func TestValidateByteArrays(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 {
+			t.Error(err)
+		}
+	})
+
+	t.Run("errTooShort", func(t *testing.T) {
+		var b []byte
+		b = plain.AppendByteArrayString(b, "Hello")
+		b = plain.AppendByteArrayString(b, "World")
+		b = plain.AppendByteArrayString(b, "!")
+
+		if plain.ValidateByteArrays(b[:len(b)-1]) == nil {
+			t.Error("expected non-nil error")
+		}
+	})
+}
diff --git a/encoding/test/test_go17.go b/encoding/test/test_go17.go
new file mode 100644
index 0000000..b8cb5fe
--- /dev/null
+++ b/encoding/test/test_go17.go
@@ -0,0 +1,3 @@
+//go:build !go1.17
+
+package test
diff --git a/encoding/test/test.go b/encoding/test/test_go18.go
similarity index 71%
rename from encoding/test/test.go
rename to encoding/test/test_go18.go
index a022dce..187bef8 100644
--- a/encoding/test/test.go
+++ b/encoding/test/test_go18.go
@@ -10,6 +10,38 @@ import (
 	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
+func EncodeInt32(t *testing.T, enc encoding.Encoding, min, max int, bitWidth uint) {
+	t.Helper()
+	encode(t, enc, min, max,
+		encoding.Encoding.EncodeInt32,
+		encoding.Encoding.DecodeInt32,
+		func(i int) int32 {
+			value := int32(i)
+			mask := int32((1 << bitWidth) - 1)
+			if (i % 2) != 0 {
+				value = -value
+			}
+			return value & mask
+		},
+	)
+}
+
+func EncodeInt64(t *testing.T, enc encoding.Encoding, min, max int, bitWidth uint) {
+	t.Helper()
+	encode(t, enc, min, max,
+		encoding.Encoding.EncodeInt64,
+		encoding.Encoding.DecodeInt64,
+		func(i int) int64 {
+			value := int64(i)
+			mask := int64((1 << bitWidth) - 1)
+			if (i % 2) != 0 {
+				value = -value
+			}
+			return value & mask
+		},
+	)
+}
+
 func EncodeFloat(t *testing.T, enc encoding.Encoding, min, max int) {
 	t.Helper()
 	encode(t, enc, min, max,
diff --git a/internal/bits/add_test.go b/internal/bits/add_test.go
index 04db284..d541732 100644
--- a/internal/bits/add_test.go
+++ b/internal/bits/add_test.go
@@ -5,10 +5,11 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func TestAddInt32(t *testing.T) {
-	err := quickCheck(func(values []int32) bool {
+	err := quick.Check(func(values []int32) bool {
 		const add = math.MaxInt32
 
 		valuesAndAdd := make([]int32, len(values))
@@ -32,7 +33,7 @@ func TestAddInt32(t *testing.T) {
 }
 
 func TestAddInt64(t *testing.T) {
-	err := quickCheck(func(values []int64) bool {
+	err := quick.Check(func(values []int64) bool {
 		const add = math.MaxInt64
 
 		valuesAndAdd := make([]int64, len(values))
diff --git a/internal/bits/bits_test.go b/internal/bits/bits_test.go
index 9612957..39f478e 100644
--- a/internal/bits/bits_test.go
+++ b/internal/bits/bits_test.go
@@ -2,8 +2,6 @@ package bits_test
 
 import (
 	"fmt"
-	"math/rand"
-	"reflect"
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
@@ -112,119 +110,3 @@ func forEachBenchmarkBufferSize(b *testing.B, f func(*testing.B, int)) {
 		})
 	}
 }
-
-// quickCheck is inspired by the standard quick.Check package, but enhances the
-// API and tests arrays of larger sizes than the maximum of 50 hardcoded in
-// testing/quick.
-func quickCheck(f interface{}) error {
-	v := reflect.ValueOf(f)
-	r := rand.New(rand.NewSource(0))
-
-	var makeArray func(int) interface{}
-	switch t := v.Type().In(0); t.Elem().Kind() {
-	case reflect.Bool:
-		makeArray = func(n int) interface{} {
-			v := make([]bool, n)
-			for i := range v {
-				v[i] = r.Int()%2 != 0
-			}
-			return v
-		}
-
-	case reflect.Int32:
-		makeArray = func(n int) interface{} {
-			v := make([]int32, n)
-			for i := range v {
-				v[i] = r.Int31()
-			}
-			return v
-		}
-
-	case reflect.Int64:
-		makeArray = func(n int) interface{} {
-			v := make([]int64, n)
-			for i := range v {
-				v[i] = r.Int63()
-			}
-			return v
-		}
-
-	case reflect.Uint32:
-		makeArray = func(n int) interface{} {
-			v := make([]uint32, n)
-			for i := range v {
-				v[i] = r.Uint32()
-			}
-			return v
-		}
-	case reflect.Uint64:
-		makeArray = func(n int) interface{} {
-			v := make([]uint64, n)
-			for i := range v {
-				v[i] = r.Uint64()
-			}
-			return v
-		}
-
-	case reflect.Float32:
-		makeArray = func(n int) interface{} {
-			v := make([]float32, n)
-			for i := range v {
-				v[i] = r.Float32()
-			}
-			return v
-		}
-
-	case reflect.Float64:
-		makeArray = func(n int) interface{} {
-			v := make([]float64, n)
-			for i := range v {
-				v[i] = r.Float64()
-			}
-			return v
-		}
-
-	case reflect.Uint8:
-		makeArray = func(n int) interface{} {
-			v := make([]byte, n)
-			r.Read(v)
-			return v
-		}
-
-	case reflect.Array:
-		e := t.Elem()
-		if e.Elem().Kind() == reflect.Uint8 && e.Len() == 16 {
-			makeArray = func(n int) interface{} {
-				v := make([]byte, n*16)
-				r.Read(v)
-				return bits.BytesToUint128(v)
-			}
-		}
-	}
-
-	if makeArray == nil {
-		panic("cannot run quick check on function with input of type " + v.Type().In(0).String())
-	}
-
-	for _, n := range [...]int{
-		0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
-		10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
-		20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
-		30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
-		99, 100, 101,
-		127, 128, 129,
-		255, 256, 257,
-		1000, 1023, 1024, 1025,
-		2000, 2095, 2048, 2049,
-		4000, 4095, 4096, 4097,
-	} {
-		for i := 0; i < 3; i++ {
-			in := makeArray(n)
-			ok := v.Call([]reflect.Value{reflect.ValueOf(in)})
-			if !ok[0].Bool() {
-				return fmt.Errorf("test #%d: failed on input of size %d: %#v\n", i+1, n, in)
-			}
-		}
-	}
-	return nil
-}
diff --git a/internal/bits/count_test.go b/internal/bits/count_test.go
index 0e25338..1add19c 100644
--- a/internal/bits/count_test.go
+++ b/internal/bits/count_test.go
@@ -5,10 +5,11 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func TestCountByte(t *testing.T) {
-	err := quickCheck(func(data []byte) bool {
+	err := quick.Check(func(data []byte) bool {
 		data = bytes.Repeat(data, 8)
 		for _, c := range data {
 			n1 := bytes.Count(data, []byte{c})
diff --git a/internal/bits/max_test.go b/internal/bits/max_test.go
index c0b1668..f03c64a 100644
--- a/internal/bits/max_test.go
+++ b/internal/bits/max_test.go
@@ -6,10 +6,11 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func TestMaxBool(t *testing.T) {
-	err := quickCheck(func(values []bool) bool {
+	err := quick.Check(func(values []bool) bool {
 		max := false
 		for _, v := range values {
 			if v {
@@ -36,7 +37,7 @@ func TestMaxBool(t *testing.T) {
 }
 
 func TestMaxInt32(t *testing.T) {
-	err := quickCheck(func(values []int32) bool {
+	err := quick.Check(func(values []int32) bool {
 		max := int32(0)
 		if len(values) > 0 {
 			max = values[0]
@@ -54,7 +55,7 @@ func TestMaxInt32(t *testing.T) {
 }
 
 func TestMaxInt64(t *testing.T) {
-	err := quickCheck(func(values []int64) bool {
+	err := quick.Check(func(values []int64) bool {
 		max := int64(0)
 		if len(values) > 0 {
 			max = values[0]
@@ -72,7 +73,7 @@ func TestMaxInt64(t *testing.T) {
 }
 
 func TestMaxUint32(t *testing.T) {
-	err := quickCheck(func(values []uint32) bool {
+	err := quick.Check(func(values []uint32) bool {
 		max := uint32(0)
 		if len(values) > 0 {
 			max = values[0]
@@ -90,7 +91,7 @@ func TestMaxUint32(t *testing.T) {
 }
 
 func TestMaxUint64(t *testing.T) {
-	err := quickCheck(func(values []uint64) bool {
+	err := quick.Check(func(values []uint64) bool {
 		max := uint64(0)
 		if len(values) > 0 {
 			max = values[0]
@@ -108,7 +109,7 @@ func TestMaxUint64(t *testing.T) {
 }
 
 func TestMaxFloat32(t *testing.T) {
-	err := quickCheck(func(values []float32) bool {
+	err := quick.Check(func(values []float32) bool {
 		max := float32(0)
 		if len(values) > 0 {
 			max = values[0]
@@ -126,7 +127,7 @@ func TestMaxFloat32(t *testing.T) {
 }
 
 func TestMaxFloat64(t *testing.T) {
-	err := quickCheck(func(values []float64) bool {
+	err := quick.Check(func(values []float64) bool {
 		max := float64(0)
 		if len(values) > 0 {
 			max = values[0]
@@ -144,7 +145,7 @@ func TestMaxFloat64(t *testing.T) {
 }
 
 func TestMaxFixedLenByteArray1(t *testing.T) {
-	err := quickCheck(func(values []byte) bool {
+	err := quick.Check(func(values []byte) bool {
 		max := [1]byte{}
 		if len(values) > 0 {
 			max[0] = values[0]
@@ -163,7 +164,7 @@ func TestMaxFixedLenByteArray1(t *testing.T) {
 }
 
 func TestMaxFixedLenByteArray16(t *testing.T) {
-	err := quickCheck(func(values [][16]byte) bool {
+	err := quick.Check(func(values [][16]byte) bool {
 		max := [16]byte{}
 		if len(values) > 0 {
 			max = values[0]
diff --git a/internal/bits/min_test.go b/internal/bits/min_test.go
index 2f175cf..3daff50 100644
--- a/internal/bits/min_test.go
+++ b/internal/bits/min_test.go
@@ -6,10 +6,11 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func TestMinBool(t *testing.T) {
-	err := quickCheck(func(values []bool) bool {
+	err := quick.Check(func(values []bool) bool {
 		min := len(values) > 0
 		for _, v := range values {
 			if !v {
@@ -36,7 +37,7 @@ func TestMinBool(t *testing.T) {
 }
 
 func TestMinInt32(t *testing.T) {
-	err := quickCheck(func(values []int32) bool {
+	err := quick.Check(func(values []int32) bool {
 		min := int32(0)
 		if len(values) > 0 {
 			min = values[0]
@@ -54,7 +55,7 @@ func TestMinInt32(t *testing.T) {
 }
 
 func TestMinInt64(t *testing.T) {
-	err := quickCheck(func(values []int64) bool {
+	err := quick.Check(func(values []int64) bool {
 		min := int64(0)
 		if len(values) > 0 {
 			min = values[0]
@@ -72,7 +73,7 @@ func TestMinInt64(t *testing.T) {
 }
 
 func TestMinUint32(t *testing.T) {
-	err := quickCheck(func(values []uint32) bool {
+	err := quick.Check(func(values []uint32) bool {
 		min := uint32(0)
 		if len(values) > 0 {
 			min = values[0]
@@ -90,7 +91,7 @@ func TestMinUint32(t *testing.T) {
 }
 
 func TestMinUint64(t *testing.T) {
-	err := quickCheck(func(values []uint64) bool {
+	err := quick.Check(func(values []uint64) bool {
 		min := uint64(0)
 		if len(values) > 0 {
 			min = values[0]
@@ -108,7 +109,7 @@ func TestMinUint64(t *testing.T) {
 }
 
 func TestMinFloat32(t *testing.T) {
-	err := quickCheck(func(values []float32) bool {
+	err := quick.Check(func(values []float32) bool {
 		min := float32(0)
 		if len(values) > 0 {
 			min = values[0]
@@ -126,7 +127,7 @@ func TestMinFloat32(t *testing.T) {
 }
 
 func TestMinFloat64(t *testing.T) {
-	err := quickCheck(func(values []float64) bool {
+	err := quick.Check(func(values []float64) bool {
 		min := float64(0)
 		if len(values) > 0 {
 			min = values[0]
@@ -144,7 +145,7 @@ func TestMinFloat64(t *testing.T) {
 }
 
 func TestMinFixedLenByteArray1(t *testing.T) {
-	err := quickCheck(func(values []byte) bool {
+	err := quick.Check(func(values []byte) bool {
 		min := [1]byte{}
 		if len(values) > 0 {
 			min[0] = values[0]
@@ -163,7 +164,7 @@ func TestMinFixedLenByteArray1(t *testing.T) {
 }
 
 func TestMinFixedLenByteArray16(t *testing.T) {
-	err := quickCheck(func(values [][16]byte) bool {
+	err := quick.Check(func(values [][16]byte) bool {
 		min := [16]byte{}
 		if len(values) > 0 {
 			min = values[0]
diff --git a/internal/bits/minmax_test.go b/internal/bits/minmax_test.go
index 9d8c3b9..c006e52 100644
--- a/internal/bits/minmax_test.go
+++ b/internal/bits/minmax_test.go
@@ -6,12 +6,13 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 const scaleFactor = 6000
 
 func TestMinMaxBool(t *testing.T) {
-	err := quickCheck(func(values []bool) bool {
+	err := quick.Check(func(values []bool) bool {
 		min := len(values) > 0
 		max := false
 		for _, v := range values {
@@ -41,7 +42,7 @@ func TestMinMaxBool(t *testing.T) {
 }
 
 func TestMinMaxInt32(t *testing.T) {
-	err := quickCheck(func(values []int32) bool {
+	err := quick.Check(func(values []int32) bool {
 		min := int32(0)
 		max := int32(0)
 		if len(values) > 0 {
@@ -65,7 +66,7 @@ func TestMinMaxInt32(t *testing.T) {
 }
 
 func TestMinMaxInt64(t *testing.T) {
-	err := quickCheck(func(values []int64) bool {
+	err := quick.Check(func(values []int64) bool {
 		min := int64(0)
 		max := int64(0)
 		if len(values) > 0 {
@@ -89,7 +90,7 @@ func TestMinMaxInt64(t *testing.T) {
 }
 
 func TestMinMaxUint32(t *testing.T) {
-	err := quickCheck(func(values []uint32) bool {
+	err := quick.Check(func(values []uint32) bool {
 		min := uint32(0)
 		max := uint32(0)
 		if len(values) > 0 {
@@ -113,7 +114,7 @@ func TestMinMaxUint32(t *testing.T) {
 }
 
 func TestMinMaxUint64(t *testing.T) {
-	err := quickCheck(func(values []uint64) bool {
+	err := quick.Check(func(values []uint64) bool {
 		min := uint64(0)
 		max := uint64(0)
 		if len(values) > 0 {
@@ -137,7 +138,7 @@ func TestMinMaxUint64(t *testing.T) {
 }
 
 func TestMinMaxFloat32(t *testing.T) {
-	err := quickCheck(func(values []float32) bool {
+	err := quick.Check(func(values []float32) bool {
 		min := float32(0)
 		max := float32(0)
 		if len(values) > 0 {
@@ -161,7 +162,7 @@ func TestMinMaxFloat32(t *testing.T) {
 }
 
 func TestMinMaxFloat64(t *testing.T) {
-	err := quickCheck(func(values []float64) bool {
+	err := quick.Check(func(values []float64) bool {
 		min := float64(0)
 		max := float64(0)
 		if len(values) > 0 {
@@ -185,7 +186,7 @@ func TestMinMaxFloat64(t *testing.T) {
 }
 
 func TestMinMaxFixedLenByteArray1(t *testing.T) {
-	err := quickCheck(func(values []byte) bool {
+	err := quick.Check(func(values []byte) bool {
 		min := [1]byte{}
 		max := [1]byte{}
 		if len(values) > 0 {
@@ -210,7 +211,7 @@ func TestMinMaxFixedLenByteArray1(t *testing.T) {
 }
 
 func TestMinMaxFixedLenByteArray16(t *testing.T) {
-	err := quickCheck(func(values [][16]byte) bool {
+	err := quick.Check(func(values [][16]byte) bool {
 		min := [16]byte{}
 		max := [16]byte{}
 		if len(values) > 0 {
diff --git a/internal/bits/order_test.go b/internal/bits/order_test.go
index 2533658..e3cff27 100644
--- a/internal/bits/order_test.go
+++ b/internal/bits/order_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 type boolOrder []bool
@@ -115,7 +116,7 @@ func TestOrderOfBool(t *testing.T) {
 	check := func(values []bool) bool {
 		return checkOrdering(t, boolOrder(values), bits.OrderOfBool(values))
 	}
-	err := quickCheck(func(values []bool) bool {
+	err := quick.Check(func(values []bool) bool {
 		if !check(values) {
 			return false
 		}
@@ -138,7 +139,7 @@ func TestOrderOfInt32(t *testing.T) {
 	check := func(values []int32) bool {
 		return checkOrdering(t, int32Order(values), bits.OrderOfInt32(values))
 	}
-	err := quickCheck(func(values []int32) bool {
+	err := quick.Check(func(values []int32) bool {
 		if !check(values) {
 			return false
 		}
@@ -176,7 +177,7 @@ func TestOrderOfInt64(t *testing.T) {
 	check := func(values []int64) bool {
 		return checkOrdering(t, int64Order(values), bits.OrderOfInt64(values))
 	}
-	err := quickCheck(func(values []int64) bool {
+	err := quick.Check(func(values []int64) bool {
 		if !check(values) {
 			return false
 		}
@@ -210,7 +211,7 @@ func TestOrderOfUint32(t *testing.T) {
 	check := func(values []uint32) bool {
 		return checkOrdering(t, uint32Order(values), bits.OrderOfUint32(values))
 	}
-	err := quickCheck(func(values []uint32) bool {
+	err := quick.Check(func(values []uint32) bool {
 		if !check(values) {
 			return false
 		}
@@ -244,7 +245,7 @@ func TestOrderOfUint64(t *testing.T) {
 	check := func(values []uint64) bool {
 		return checkOrdering(t, uint64Order(values), bits.OrderOfUint64(values))
 	}
-	err := quickCheck(func(values []uint64) bool {
+	err := quick.Check(func(values []uint64) bool {
 		if !check(values) {
 			return false
 		}
@@ -278,7 +279,7 @@ func TestOrderOfFloat32(t *testing.T) {
 	check := func(values []float32) bool {
 		return checkOrdering(t, float32Order(values), bits.OrderOfFloat32(values))
 	}
-	err := quickCheck(func(values []float32) bool {
+	err := quick.Check(func(values []float32) bool {
 		if !check(values) {
 			return false
 		}
@@ -312,7 +313,7 @@ func TestOrderOfFloat64(t *testing.T) {
 	check := func(values []float64) bool {
 		return checkOrdering(t, float64Order(values), bits.OrderOfFloat64(values))
 	}
-	err := quickCheck(func(values []float64) bool {
+	err := quick.Check(func(values []float64) bool {
 		if !check(values) {
 			return false
 		}
@@ -346,7 +347,7 @@ func TestOrderOfBytes(t *testing.T) {
 	check := func(values [][]byte) bool {
 		return checkOrdering(t, bytesOrder(values), bits.OrderOfBytes(values))
 	}
-	err := quickCheck(func(values [][16]byte) bool {
+	err := quick.Check(func(values [][16]byte) bool {
 		slices := make([][]byte, len(values))
 		for i := range values {
 			slices[i] = values[i][:]
diff --git a/internal/bits/sub_test.go b/internal/bits/sub_test.go
index ec038fc..caa71a1 100644
--- a/internal/bits/sub_test.go
+++ b/internal/bits/sub_test.go
@@ -5,10 +5,11 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func TestSubInt32(t *testing.T) {
-	err := quickCheck(func(values []int32) bool {
+	err := quick.Check(func(values []int32) bool {
 		const sub = math.MaxInt32
 
 		valuesAndSub := make([]int32, len(values))
@@ -32,7 +33,7 @@ func TestSubInt32(t *testing.T) {
 }
 
 func TestSubInt64(t *testing.T) {
-	err := quickCheck(func(values []int64) bool {
+	err := quick.Check(func(values []int64) bool {
 		const sub = math.MaxInt64
 
 		valuesAndSub := make([]int64, len(values))
diff --git a/parquet_test.go b/parquet_test.go
index 134af2e..ba20232 100644
--- a/parquet_test.go
+++ b/parquet_test.go
@@ -13,11 +13,12 @@ import (
 	"github.com/google/uuid"
 	"github.com/segmentio/parquet-go"
 	"github.com/segmentio/parquet-go/deprecated"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 const (
 	benchmarkNumRows     = 10_000
-	benchmarkRowsPerStep = 100
+	benchmarkRowsPerStep = 1000
 )
 
 type benchmarkRowType struct {
@@ -31,6 +32,15 @@ func (row benchmarkRowType) generate(prng *rand.Rand) benchmarkRowType {
 	return row
 }
 
+type paddedBooleanColumn struct {
+	Value bool
+	_     [3]byte
+}
+
+func (row paddedBooleanColumn) generate(prng *rand.Rand) paddedBooleanColumn {
+	return paddedBooleanColumn{Value: prng.Int()%2 == 0}
+}
+
 type booleanColumn struct {
 	Value bool
 }
@@ -40,7 +50,7 @@ func (row booleanColumn) generate(prng *rand.Rand) booleanColumn {
 }
 
 type int32Column struct {
-	Value int32
+	Value int32 `parquet:",delta"`
 }
 
 func (row int32Column) generate(prng *rand.Rand) int32Column {
@@ -418,3 +428,14 @@ func generateString(r *rand.Rand, n int) string {
 	}
 	return b.String()
 }
+
+var quickCheckConfig = quick.Config{
+	Sizes: []int{
+		0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+		10, 20, 30, 40, 50, 123,
+	},
+}
+
+func quickCheck(f interface{}) error {
+	return quickCheckConfig.Check(f)
+}
diff --git a/reader_go18_test.go b/reader_go18_test.go
index a48e927..0bbb66c 100644
--- a/reader_go18_test.go
+++ b/reader_go18_test.go
@@ -10,7 +10,6 @@ import (
 	"math/rand"
 	"reflect"
 	"testing"
-	"testing/quick"
 
 	"github.com/segmentio/parquet-go"
 )
@@ -40,10 +39,9 @@ func TestGenericReader(t *testing.T) {
 }
 
 func testGenericReader[Row any](t *testing.T) {
-	var prng = rand.New(rand.NewSource(2))
 	var model Row
 	t.Run(reflect.TypeOf(model).Name(), func(t *testing.T) {
-		f := func(rows []Row) bool {
+		err := quickCheck(func(rows []Row) bool {
 			if len(rows) == 0 {
 				return true // TODO: fix support for parquet files with zero rows
 			}
@@ -52,8 +50,8 @@ func testGenericReader[Row any](t *testing.T) {
 				return false
 			}
 			return true
-		}
-		if err := quick.Check(f, &quick.Config{Rand: prng}); err != nil {
+		})
+		if err != nil {
 			t.Error(err)
 		}
 	})
diff --git a/reader_test.go b/reader_test.go
index a82c63a..78a5493 100644
--- a/reader_test.go
+++ b/reader_test.go
@@ -8,9 +8,9 @@ import (
 	"math/rand"
 	"reflect"
 	"testing"
-	"testing/quick"
 
 	"github.com/segmentio/parquet-go"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func rowsOf(numRows int, model interface{}) rows {
@@ -21,11 +21,10 @@ func rowsOf(numRows int, model interface{}) rows {
 func randomRowsOf(prng *rand.Rand, numRows int, model interface{}) rows {
 	typ := reflect.TypeOf(model)
 	rows := make(rows, numRows)
+	makeValue := quick.MakeValueFuncOf(typ)
 	for i := range rows {
-		v, ok := quick.Value(typ, prng)
-		if !ok {
-			panic("cannot generate random value for test")
-		}
+		v := reflect.New(typ).Elem()
+		makeValue(v, prng)
 		rows[i] = v.Interface()
 	}
 	return rows
@@ -290,7 +289,7 @@ func TestReaderReadSubset(t *testing.T) {
 	type Point3D struct{ X, Y, Z int64 }
 	type Point2D struct{ X, Y int64 }
 
-	f := func(points3D []Point3D) bool {
+	err := quickCheck(func(points3D []Point3D) bool {
 		if len(points3D) == 0 {
 			return true
 		}
@@ -317,8 +316,8 @@ func TestReaderReadSubset(t *testing.T) {
 			}
 		}
 		return true
-	}
-	if err := quick.Check(f, nil); err != nil {
+	})
+	if err != nil {
 		t.Error(err)
 	}
 }
diff --git a/row_group_test.go b/row_group_test.go
index ba2245d..4474438 100644
--- a/row_group_test.go
+++ b/row_group_test.go
@@ -2,11 +2,9 @@ package parquet_test
 
 import (
 	"bytes"
-	"math/rand"
 	"reflect"
 	"sort"
 	"testing"
-	"testing/quick"
 
 	"github.com/segmentio/parquet-go"
 )
@@ -66,7 +64,7 @@ func TestSeekToRow(t *testing.T) {
 }
 
 func testSeekToRow(t *testing.T, newRowGroup func([]Person) parquet.RowGroup) {
-	f := func(people []Person) bool {
+	err := quickCheck(func(people []Person) bool {
 		if len(people) == 0 { // TODO: fix creation of empty parquet files
 			return true
 		}
@@ -97,10 +95,8 @@ func testSeekToRow(t *testing.T, newRowGroup func([]Person) parquet.RowGroup) {
 		}
 
 		return true
-	}
-	if err := quick.Check(f, &quick.Config{
-		Rand: rand.New(rand.NewSource(3)),
-	}); err != nil {
+	})
+	if err != nil {
 		t.Error(err)
 	}
 }
diff --git a/row_test.go b/row_test.go
index b1f7fad..f6a782c 100644
--- a/row_test.go
+++ b/row_test.go
@@ -8,6 +8,16 @@ import (
 	"github.com/segmentio/parquet-go"
 )
 
+func TestRowClone(t *testing.T) {
+	row := parquet.Row{
+		parquet.ValueOf(42).Level(0, 1, 0),
+		parquet.ValueOf("Hello World").Level(1, 1, 1),
+	}
+	if clone := row.Clone(); !row.Equal(clone) {
+		t.Error("row and its clone are not equal")
+	}
+}
+
 func TestDeconstructionReconstruction(t *testing.T) {
 	type Person struct {
 		FirstName string
diff --git a/schema_test.go b/schema_test.go
index 50cb976..ea2c34b 100644
--- a/schema_test.go
+++ b/schema_test.go
@@ -52,6 +52,46 @@ func TestSchemaOf(t *testing.T) {
 		required binary first_name (STRING);
 		required binary last_name (STRING);
 	}
+}`,
+		},
+
+		{
+			value: new(struct {
+				Short float32 `parquet:"short,split"`
+				Long  float64 `parquet:"long,split"`
+			}),
+			print: `message {
+	required float short;
+	required double long;
+}`,
+		},
+
+		{
+			value: new(struct {
+				Inner struct {
+					FirstName          string `parquet:"first_name"`
+					ShouldNotBePresent string `parquet:"-"`
+				} `parquet:"inner,optional"`
+			}),
+			print: `message {
+	optional group inner {
+		required binary first_name (STRING);
+	}
+}`,
+		},
+
+		{
+			value: new(struct {
+				Inner struct {
+					FirstName    string `parquet:"first_name"`
+					MyNameIsDash string `parquet:"-,"`
+				} `parquet:"inner,optional"`
+			}),
+			print: `message {
+	optional group inner {
+		required binary first_name (STRING);
+		required binary - (STRING);
+	}
 }`,
 		},
 	}
diff --git a/writer_test.go b/writer_test.go
index 2fc53b0..c8655e3 100644
--- a/writer_test.go
+++ b/writer_test.go
@@ -7,7 +7,6 @@ import (
 	"os/exec"
 	"strings"
 	"testing"
-	"testing/quick"
 
 	"github.com/google/uuid"
 	"github.com/hexops/gotextdiff"
@@ -78,6 +77,13 @@ type timeseries struct {
 	Value     float64 `parquet:"value"`
 }
 
+type event struct {
+	Name     string  `parquet:"name,dict"`
+	Type     string  `parquet:"-"`
+	Value    float64 `parquet:"value"`
+	Category string  `parquet:"-"`
+}
+
 var writerTests = []struct {
 	scenario string
 	version  int
@@ -277,10 +283,10 @@ value 10: R:0 D:0 V:10.0
 		dump: `row group 0
 --------------------------------------------------------------------------------
 owner:              BINARY ZSTD DO:0 FPO:4 SZ:66/57/0.86 VC:2 ENC:DELTA_LENGTH_BYTE_ARRAY ST:[no stats for this column]
-ownerPhoneNumbers:  BINARY GZIP DO:0 FPO:70 SZ:162/112/0.69 VC:3 ENC:DELTA_LENGTH_BYTE_ARRAY,RLE ST:[no stats for this column]
+ownerPhoneNumbers:  BINARY GZIP DO:0 FPO:70 SZ:162/112/0.69 VC:3 ENC:RLE,DELTA_LENGTH_BYTE_ARRAY ST:[no stats for this column]
 contacts:
-.name:              BINARY UNCOMPRESSED DO:0 FPO:232 SZ:116/116/1.00 VC:3 ENC:DELTA_LENGTH_BYTE_ARRAY,RLE ST:[no stats for this column]
-.phoneNumber:       BINARY ZSTD DO:0 FPO:348 SZ:113/95/0.84 VC:3 ENC:DELTA_LENGTH_BYTE_ARRAY,RLE ST:[no stats for this column]
+.name:              BINARY UNCOMPRESSED DO:0 FPO:232 SZ:116/116/1.00 VC:3 ENC:RLE,DELTA_LENGTH_BYTE_ARRAY ST:[no stats for this column]
+.phoneNumber:       BINARY ZSTD DO:0 FPO:348 SZ:113/95/0.84 VC:3 ENC:RLE,DELTA_LENGTH_BYTE_ARRAY ST:[no stats for this column]
 
     owner TV=2 RL=0 DL=0
     ----------------------------------------------------------------------------
@@ -359,10 +365,10 @@ value 3: R:0 D:0 V:<null>
 		dump: `row group 0
 --------------------------------------------------------------------------------
 owner:              BINARY ZSTD DO:0 FPO:4 SZ:71/62/0.87 VC:2 ENC:DELTA_LENGTH_BYTE_ARRAY ST:[no stats for this column]
-ownerPhoneNumbers:  BINARY GZIP DO:0 FPO:75 SZ:156/106/0.68 VC:3 ENC:DELTA_LENGTH_BYTE_ARRAY,RLE ST:[no stats for this column]
+ownerPhoneNumbers:  BINARY GZIP DO:0 FPO:75 SZ:156/106/0.68 VC:3 ENC:RLE,DELTA_LENGTH_BYTE_ARRAY ST:[no stats for this column]
 contacts:
-.name:              BINARY UNCOMPRESSED DO:0 FPO:231 SZ:110/110/1.00 VC:3 ENC:DELTA_LENGTH_BYTE_ARRAY,RLE ST:[no stats for this column]
-.phoneNumber:       BINARY ZSTD DO:0 FPO:341 SZ:108/90/0.83 VC:3 ENC:DELTA_LENGTH_BYTE_ARRAY,RLE ST:[no stats for this column]
+.name:              BINARY UNCOMPRESSED DO:0 FPO:231 SZ:110/110/1.00 VC:3 ENC:RLE,DELTA_LENGTH_BYTE_ARRAY ST:[no stats for this column]
+.phoneNumber:       BINARY ZSTD DO:0 FPO:341 SZ:108/90/0.83 VC:3 ENC:RLE,DELTA_LENGTH_BYTE_ARRAY ST:[no stats for this column]
 
     owner TV=2 RL=0 DL=0
     ----------------------------------------------------------------------------
@@ -409,6 +415,40 @@ BINARY contacts.phoneNumber
 value 1: R:0 D:2 V:555 987 6543
 value 2: R:1 D:1 V:<null>
 value 3: R:0 D:0 V:<null>
+`,
+	},
+
+	{
+		scenario: "omit `-` fields",
+		version:  v1,
+		rows: []interface{}{
+			&event{Name: "customer1", Type: "request", Value: 42.0},
+			&event{Name: "customer2", Type: "access", Value: 1.0},
+		},
+		dump: `row group 0
+--------------------------------------------------------------------------------
+name:   BINARY UNCOMPRESSED DO:4 FPO:49 SZ:73/73/1.00 VC:2 ENC:PLAIN,RLE_DICTIONARY ST:[no stats for this column]
+value:  DOUBLE UNCOMPRESSED DO:0 FPO:77 SZ:39/39/1.00 VC:2 ENC:PLAIN ST:[no stats for this column]
+
+    name TV=2 RL=0 DL=0 DS: 2 DE:PLAIN
+    ----------------------------------------------------------------------------
+    page 0:                  DLE:RLE RLE:RLE VLE:RLE_DICTIONARY ST:[no stats for this column] CRC:[verified] SZ:5 VC:2
+
+    value TV=2 RL=0 DL=0
+    ----------------------------------------------------------------------------
+    page 0:                  DLE:RLE RLE:RLE VLE:PLAIN ST:[no stats for this column] CRC:[verified] SZ:16 VC:2
+
+BINARY name
+--------------------------------------------------------------------------------
+*** row group 1 of 1, values 1 to 2 ***
+value 1: R:0 D:0 V:customer1
+value 2: R:0 D:0 V:customer2
+
+DOUBLE value
+--------------------------------------------------------------------------------
+*** row group 1 of 1, values 1 to 2 ***
+value 1: R:0 D:0 V:42.0
+value 2: R:0 D:0 V:1.0
 `,
 	},
 }
@@ -474,7 +514,7 @@ func TestWriterGenerateBloomFilters(t *testing.T) {
 		LastName  utf8string `parquet:"last_name"`
 	}
 
-	f := func(rows []Person) bool {
+	err := quickCheck(func(rows []Person) bool {
 		if len(rows) == 0 { // TODO: support writing files with no rows
 			return true
 		}
@@ -529,9 +569,8 @@ func TestWriterGenerateBloomFilters(t *testing.T) {
 		}
 
 		return true
-	}
-
-	if err := quick.Check(f, nil); err != nil {
+	})
+	if err != nil {
 		t.Error(err)
 	}
 }