Repository: segmentio/parquet-go
Pull request #227 (2022-06-09 23:01:01)

Allow slice for FixedLengthByteArray decimals to improve performance
At the moment for fixed length byte array decimals the array is never addressable so it calls reflect.New for each value. There is some code to check the addressability of the array but my understanding is that it will never be addressable requiring this allocation each time. Using a slice of the correct size allows skipping this allocation.

For pointer fields currently the schema functions will show the kind as reflect.Pointer and cause it to throw an invalid field tag error. Added some code to check if the type is a pointer and get the underlying value with Elem.

achille-roussel: `reflect.Pointer` was introduced in Go 1.18, it used to be named `reflect.Ptr` which causes a build failure with Go 1.17:

```
# github.com/segmentio/parquet-go [github.com/segmentio/parquet-go.test]
Error: ./schema.go:441:22: undefined: reflect.Pointer
```
hhoughgg: I had to make more changes as doing f.Type == f.Type.Elem caused other parts of the code to break since they do this check as well. Not sure if this is really the best way to do it but does scope the type to this single function.

Patch:
diff --git a/column_buffer.go b/column_buffer.go
index 80b81e7..dfb07c3 100644
--- a/column_buffer.go
+++ b/column_buffer.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/segmentio/parquet-go/deprecated"
 	"github.com/segmentio/parquet-go/encoding/plain"
+	"github.com/segmentio/parquet-go/internal/bitpack"
 	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
@@ -589,10 +590,10 @@ func (col *repeatedColumnBuffer) Less(i, j int) bool {
 	row2Length := repeatedRowLength(col.repetitionLevels[row2.offset:])
 
 	for k := 0; k < row1Length && k < row2Length; k++ {
-		x := int(row1.offset) + k
-		y := int(row2.offset) + k
-		definitionLevel1 := col.definitionLevels[j+k]
-		definitionLevel2 := col.definitionLevels[j+k]
+		x := int(row1.baseOffset)
+		y := int(row2.baseOffset)
+		definitionLevel1 := col.definitionLevels[int(row1.offset)+k]
+		definitionLevel2 := col.definitionLevels[int(row2.offset)+k]
 		switch {
 		case less(col.base, x, y, col.maxDefinitionLevel, definitionLevel1, definitionLevel2):
 			return true
@@ -616,42 +617,32 @@ func (col *repeatedColumnBuffer) Swap(i, j int) {
 }
 
 func (col *repeatedColumnBuffer) WriteValues(values []Value) (numValues int, err error) {
-	// The values may belong to the last row that was written if they do not
-	// start with a repetition level less than the column's maximum.
-	var continuation Row
-	if len(values) > 0 && values[0].repetitionLevel != 0 {
-		continuation, values = splitRowValues(values)
-	}
-
-	if len(continuation) > 0 {
-		for i, v := range continuation {
-			if v.definitionLevel == col.maxDefinitionLevel {
-				if _, err := col.base.WriteValues(continuation[i : i+1]); err != nil {
-					return numValues, err
-				}
-			}
-			col.repetitionLevels = append(col.repetitionLevels, v.repetitionLevel)
-			col.definitionLevels = append(col.definitionLevels, v.definitionLevel)
-			numValues++
-		}
-	}
-
-	maxNumValues := 0
+	maxRowLen := 0
 	defer func() {
-		clearValues(col.buffer[:maxNumValues])
+		clearValues(col.buffer[:maxRowLen])
 	}()
 
-	var row []Value
+	for i := 0; i < len(values); {
+		j := i
+
+		if values[j].repetitionLevel == 0 {
+			j++
+		}
+
+		for j < len(values) && values[j].repetitionLevel != 0 {
+			j++
+		}
 
-	for len(values) > 0 {
-		row, values = splitRowValues(values)
-		if err := col.writeRow(row); err != nil {
+		if err := col.writeRow(values[i:j]); err != nil {
 			return numValues, err
 		}
-		numValues += len(row)
-		if len(col.buffer) > maxNumValues {
-			maxNumValues = len(col.buffer)
+
+		if len(col.buffer) > maxRowLen {
+			maxRowLen = len(col.buffer)
 		}
+
+		numValues += j - i
+		i = j
 	}
 
 	return numValues, nil
@@ -659,6 +650,7 @@ func (col *repeatedColumnBuffer) WriteValues(values []Value) (numValues int, err
 
 func (col *repeatedColumnBuffer) writeRow(row []Value) error {
 	col.buffer = col.buffer[:0]
+
 	for _, v := range row {
 		if v.definitionLevel == col.maxDefinitionLevel {
 			col.buffer = append(col.buffer, v)
@@ -672,10 +664,12 @@ func (col *repeatedColumnBuffer) writeRow(row []Value) error {
 		}
 	}
 
-	col.rows = append(col.rows, region{
-		offset:     uint32(len(col.repetitionLevels)),
-		baseOffset: uint32(baseOffset),
-	})
+	if row[0].repetitionLevel == 0 {
+		col.rows = append(col.rows, region{
+			offset:     uint32(len(col.repetitionLevels)),
+			baseOffset: uint32(baseOffset),
+		})
+	}
 
 	for _, v := range row {
 		col.repetitionLevels = append(col.repetitionLevels, v.repetitionLevel)
@@ -816,7 +810,7 @@ func (col *booleanColumnBuffer) WriteValues(values []Value) (int, error) {
 }
 
 func (col *booleanColumnBuffer) writeValues(rows array, size, offset uintptr, _ columnLevels) {
-	numBytes := byteCount(uint(col.numValues) + uint(rows.len))
+	numBytes := bitpack.ByteCount(uint(col.numValues) + uint(rows.len))
 	if cap(col.bits) < numBytes {
 		col.bits = append(make([]byte, 0, 2*cap(col.bits)), col.bits...)
 	}
@@ -890,7 +884,7 @@ func (col *booleanColumnBuffer) writeValues(rows array, size, offset uintptr, _
 		i++
 	}
 
-	col.bits = col.bits[:byteCount(uint(col.numValues))]
+	col.bits = col.bits[:bitpack.ByteCount(uint(col.numValues))]
 }
 
 func (col *booleanColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
diff --git a/column_buffer_go18.go b/column_buffer_go18.go
index e919f14..5e3eaa3 100644
--- a/column_buffer_go18.go
+++ b/column_buffer_go18.go
@@ -292,7 +292,7 @@ func writeRowsFuncOfStruct(t reflect.Type, schema *Schema, path columnPath) writ
 	for i, f := range fields {
 		optional := false
 		columnPath := path.append(f.Name)
-		forEachStructTagOption(f.Tag, func(option, _ string) {
+		forEachStructTagOption(f, func(_ reflect.Type, option, _ string) {
 			switch option {
 			case "list":
 				columnPath = columnPath.append("list", "element")
diff --git a/dictionary.go b/dictionary.go
index 4bd0ec8..a69e543 100644
--- a/dictionary.go
+++ b/dictionary.go
@@ -8,6 +8,7 @@ import (
 	"github.com/segmentio/parquet-go/deprecated"
 	"github.com/segmentio/parquet-go/encoding"
 	"github.com/segmentio/parquet-go/encoding/plain"
+	"github.com/segmentio/parquet-go/internal/bitpack"
 	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
@@ -104,7 +105,7 @@ func newBooleanDictionary(typ Type, columnIndex int16, numValues int32, values [
 	return &booleanDictionary{
 		booleanPage: booleanPage{
 			typ:         typ,
-			bits:        values[:byteCount(uint(numValues))],
+			bits:        values[:bitpack.ByteCount(uint(numValues))],
 			numValues:   numValues,
 			columnIndex: ^columnIndex,
 		},
diff --git a/encoding/delta/binary_packed.go b/encoding/delta/binary_packed.go
index 16217cf..6e47e84 100644
--- a/encoding/delta/binary_packed.go
+++ b/encoding/delta/binary_packed.go
@@ -9,8 +9,53 @@ import (
 
 	"github.com/segmentio/parquet-go/encoding"
 	"github.com/segmentio/parquet-go/format"
+	"github.com/segmentio/parquet-go/internal/bitpack"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
+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
+}
+
+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) {
+	dst, _, err := decodeInt32(dst[:0], src)
+	return dst, e.wrap(err)
+}
+
+func (e *BinaryPackedEncoding) DecodeInt64(dst, src []byte) ([]byte, error) {
+	dst, _, err := decodeInt64(dst[:0], src)
+	return dst, e.wrap(err)
+}
+
+func (e *BinaryPackedEncoding) wrap(err error) error {
+	if err != nil {
+		err = encoding.Error(e, err)
+	}
+	return err
+}
+
 const (
 	blockSize     = 128
 	numMiniBlocks = 4
@@ -68,7 +113,7 @@ func encodeInt32Default(dst []byte, src []int32) []byte {
 		for i, bitWidth := range bitWidths {
 			if bitWidth != 0 {
 				miniBlock := (*[miniBlockSize]int32)(block[i*miniBlockSize:])
-				miniBlockPackInt32(dst[n:], miniBlock, uint(bitWidth))
+				encodeMiniBlockInt32(dst[n:], miniBlock, uint(bitWidth))
 				n += (miniBlockSize * int(bitWidth)) / 8
 			}
 		}
@@ -114,7 +159,7 @@ func encodeInt64Default(dst []byte, src []int64) []byte {
 		for i, bitWidth := range bitWidths {
 			if bitWidth != 0 {
 				miniBlock := (*[miniBlockSize]int64)(block[i*miniBlockSize:])
-				miniBlockPackInt64(dst[n:], miniBlock, uint(bitWidth))
+				encodeMiniBlockInt64(dst[n:], miniBlock, uint(bitWidth))
 				n += (miniBlockSize * int(bitWidth)) / 8
 			}
 		}
@@ -235,188 +280,134 @@ func blockBitWidthsInt64(bitWidths *[numMiniBlocks]byte, block *[blockSize]int64
 	}
 }
 
-func resize(buf []byte, size int) []byte {
-	if cap(buf) < size {
-		return grow(buf, size)
+func decodeInt32(dst, src []byte) ([]byte, []byte, error) {
+	blockSize, numMiniBlocks, totalValues, firstValue, src, err := decodeBinaryPackedHeader(src)
+	if err != nil {
+		return dst, src, err
 	}
-	if size > len(buf) {
-		clear := buf[len(buf):size]
-		for i := range clear {
-			clear[i] = 0
-		}
+	if totalValues == 0 {
+		return dst, src, nil
 	}
-	return buf[:size]
-}
-
-func grow(buf []byte, size int) []byte {
-	newCap := 2 * cap(buf)
-	if newCap < size {
-		newCap = size
+	if firstValue < math.MinInt32 || firstValue > math.MaxInt32 {
+		return dst, src, fmt.Errorf("first value out of range: %d", firstValue)
 	}
-	newBuf := make([]byte, size, newCap)
-	copy(newBuf, buf)
-	return newBuf
-}
-
-type BinaryPackedEncoding struct {
-	encoding.NotSupported
-}
 
-func (e *BinaryPackedEncoding) String() string {
-	return "DELTA_BINARY_PACKED"
-}
+	writeOffset := len(dst)
+	dst = resize(dst, len(dst)+4*totalValues)
+	out := unsafecast.BytesToInt32(dst)
+	out[writeOffset] = int32(firstValue)
+	writeOffset++
+	totalValues--
+	lastValue := int32(firstValue)
+	numValuesInMiniBlock := blockSize / numMiniBlocks
 
-func (e *BinaryPackedEncoding) Encoding() format.Encoding {
-	return format.DeltaBinaryPacked
-}
+	const padding = 16
+	miniBlockTemp := make([]byte, 256+padding)
 
-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
-}
+	for totalValues > 0 && len(src) > 0 {
+		var minDelta int64
+		var bitWidths []byte
+		minDelta, bitWidths, src, err = decodeBinaryPackedBlock(src, numMiniBlocks)
+		if err != nil {
+			return dst, src, err
+		}
 
-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
-}
+		blockOffset := writeOffset
 
-func (e *BinaryPackedEncoding) DecodeInt32(dst, src []byte) ([]byte, error) {
-	dst, _, err := e.decodeInt32(dst[:0], src)
-	return dst, e.wrap(err)
-}
+		for _, bitWidth := range bitWidths {
+			n := min(numValuesInMiniBlock, totalValues)
+			if bitWidth != 0 {
+				miniBlockSize := (numValuesInMiniBlock * int(bitWidth)) / 8
+				miniBlockData := src
+				if miniBlockSize <= len(src) {
+					miniBlockData = miniBlockData[:miniBlockSize]
+				}
+				src = src[len(miniBlockData):]
+				if cap(miniBlockData) < miniBlockSize+bitpack.Padding {
+					miniBlockTemp = resize(miniBlockTemp[:0], miniBlockSize+bitpack.Padding)
+					miniBlockData = miniBlockTemp[:copy(miniBlockTemp, miniBlockData)]
+				}
+				miniBlockData = miniBlockData[:miniBlockSize]
+				bitpack.UnpackInt32(out[writeOffset:writeOffset+n], miniBlockData, uint(bitWidth))
+			}
+			writeOffset += n
+			totalValues -= n
+			if totalValues == 0 {
+				break
+			}
+		}
 
-func (e *BinaryPackedEncoding) DecodeInt64(dst, src []byte) ([]byte, error) {
-	dst, _, err := e.decodeInt64(dst[:0], src)
-	return dst, e.wrap(err)
-}
+		lastValue = decodeBlockInt32(out[blockOffset:writeOffset], int32(minDelta), lastValue)
+	}
 
-func (e *BinaryPackedEncoding) decodeInt32(dst, src []byte) ([]byte, []byte, error) {
-	src, err := e.decode(src, func(value int64) {
-		buf := [4]byte{}
-		binary.LittleEndian.PutUint32(buf[:], uint32(value))
-		dst = append(dst, buf[:]...)
-	})
-	return dst, src, err
-}
+	if totalValues > 0 {
+		return dst, src, fmt.Errorf("%d missing values: %w", totalValues, io.ErrUnexpectedEOF)
+	}
 
-func (e *BinaryPackedEncoding) decodeInt64(dst, src []byte) ([]byte, []byte, error) {
-	src, err := e.decode(src, func(value int64) {
-		buf := [8]byte{}
-		binary.LittleEndian.PutUint64(buf[:], uint64(value))
-		dst = append(dst, buf[:]...)
-	})
-	return dst, src, err
+	return dst, src, nil
 }
 
-func (e *BinaryPackedEncoding) decode(src []byte, observe func(int64)) ([]byte, error) {
+func decodeInt64(dst, src []byte) ([]byte, []byte, error) {
 	blockSize, numMiniBlocks, totalValues, firstValue, src, err := decodeBinaryPackedHeader(src)
 	if err != nil {
-		return src, err
+		return dst, src, err
 	}
 	if totalValues == 0 {
-		return src, nil
+		return dst, src, nil
 	}
 
-	observe(firstValue)
+	writeOffset := len(dst)
+	dst = resize(dst, len(dst)+8*totalValues)
+	out := unsafecast.BytesToInt64(dst)
+	out[writeOffset] = firstValue
+	writeOffset++
 	totalValues--
 	lastValue := firstValue
 	numValuesInMiniBlock := blockSize / numMiniBlocks
 
-	block := make([]int64, 128)
-	if cap(block) < blockSize {
-		block = make([]int64, blockSize)
-	} else {
-		block = block[:blockSize]
-	}
-
-	miniBlockData := make([]byte, 256)
+	const padding = 16
+	miniBlockTemp := make([]byte, 512+padding)
 
 	for totalValues > 0 && len(src) > 0 {
 		var minDelta int64
 		var bitWidths []byte
 		minDelta, bitWidths, src, err = decodeBinaryPackedBlock(src, numMiniBlocks)
 		if err != nil {
-			return src, err
-		}
-
-		blockOffset := 0
-		for i := range block {
-			block[i] = 0
+			return dst, src, err
 		}
+		blockOffset := writeOffset
 
 		for _, bitWidth := range bitWidths {
-			if bitWidth == 0 {
-				n := numValuesInMiniBlock
-				if n > totalValues {
-					n = totalValues
-				}
-				blockOffset += n
-				totalValues -= n
-			} else {
+			n := min(numValuesInMiniBlock, totalValues)
+			if bitWidth != 0 {
 				miniBlockSize := (numValuesInMiniBlock * int(bitWidth)) / 8
-				if cap(miniBlockData) < miniBlockSize {
-					miniBlockData = make([]byte, miniBlockSize, miniBlockSize)
-				} else {
-					miniBlockData = miniBlockData[:miniBlockSize]
+				miniBlockData := src
+				if miniBlockSize <= len(src) {
+					miniBlockData = src[:miniBlockSize]
 				}
-
-				n := copy(miniBlockData, src)
-				src = src[n:]
-				bitOffset := uint(0)
-
-				for count := numValuesInMiniBlock; count > 0 && totalValues > 0; count-- {
-					delta := int64(0)
-
-					for b := uint(0); b < uint(bitWidth); b++ {
-						x := (bitOffset + b) / 8
-						y := (bitOffset + b) % 8
-						delta |= int64((miniBlockData[x]>>y)&1) << b
-					}
-
-					block[blockOffset] = delta
-					blockOffset++
-					totalValues--
-					bitOffset += uint(bitWidth)
+				src = src[len(miniBlockData):]
+				if len(miniBlockData) < miniBlockSize+bitpack.Padding {
+					miniBlockTemp = resize(miniBlockTemp[:0], miniBlockSize+bitpack.Padding)
+					miniBlockData = miniBlockTemp[:copy(miniBlockTemp, miniBlockData)]
 				}
+				miniBlockData = miniBlockData[:miniBlockSize]
+				bitpack.UnpackInt64(out[writeOffset:writeOffset+n], miniBlockData, uint(bitWidth))
 			}
-
+			writeOffset += n
+			totalValues -= n
 			if totalValues == 0 {
 				break
 			}
 		}
 
-		for i := range block {
-			block[i] += minDelta
-		}
-
-		block[0] += lastValue
-		for i := 1; i < len(block); i++ {
-			block[i] += block[i-1]
-		}
-		if values := block[:blockOffset]; len(values) > 0 {
-			for _, v := range values {
-				observe(v)
-			}
-			lastValue = values[len(values)-1]
-		}
+		lastValue = decodeBlockInt64(out[blockOffset:writeOffset], minDelta, lastValue)
 	}
 
 	if totalValues > 0 {
-		return src, fmt.Errorf("%d missing values: %w", totalValues, io.ErrUnexpectedEOF)
+		return dst, src, fmt.Errorf("%d missing values: %w", totalValues, io.ErrUnexpectedEOF)
 	}
 
-	return src, nil
-}
-
-func (e *BinaryPackedEncoding) wrap(err error) error {
-	if err != nil {
-		err = encoding.Error(e, err)
-	}
-	return err
+	return dst, src, nil
 }
 
 func decodeBinaryPackedHeader(src []byte) (blockSize, numMiniBlocks, totalValues int, firstValue int64, next []byte, err error) {
diff --git a/encoding/delta/binary_packed_amd64.go b/encoding/delta/binary_packed_amd64.go
index b151285..35f9b6e 100644
--- a/encoding/delta/binary_packed_amd64.go
+++ b/encoding/delta/binary_packed_amd64.go
@@ -3,6 +3,7 @@
 package delta
 
 import (
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 	"golang.org/x/sys/cpu"
 )
 
@@ -26,36 +27,36 @@ func blockSubInt32AVX2(block *[blockSize]int32, value int32)
 func blockBitWidthsInt32AVX2(bitWidths *[numMiniBlocks]byte, block *[blockSize]int32)
 
 //go:noescape
-func miniBlockPackInt32Default(dst *byte, src *[miniBlockSize]int32, bitWidth uint)
+func encodeMiniBlockInt32Default(dst *byte, src *[miniBlockSize]int32, bitWidth uint)
 
 //go:noescape
-func miniBlockPackInt32x1bitAVX2(dst *byte, src *[miniBlockSize]int32)
+func encodeMiniBlockInt32x1bitAVX2(dst *byte, src *[miniBlockSize]int32)
 
 //go:noescape
-func miniBlockPackInt32x2bitsAVX2(dst *byte, src *[miniBlockSize]int32)
+func encodeMiniBlockInt32x2bitsAVX2(dst *byte, src *[miniBlockSize]int32)
 
 //go:noescape
-func miniBlockPackInt32x3to16bitsAVX2(dst *byte, src *[miniBlockSize]int32, bitWidth uint)
+func encodeMiniBlockInt32x3to16bitsAVX2(dst *byte, src *[miniBlockSize]int32, bitWidth uint)
 
 //go:noescape
-func miniBlockPackInt32x32bitsAVX2(dst *byte, src *[miniBlockSize]int32)
+func encodeMiniBlockInt32x32bitsAVX2(dst *byte, src *[miniBlockSize]int32)
 
-func miniBlockPackInt32(dst []byte, src *[miniBlockSize]int32, bitWidth uint) {
-	miniBlockPackInt32Default(&dst[0], src, bitWidth)
+func encodeMiniBlockInt32(dst []byte, src *[miniBlockSize]int32, bitWidth uint) {
+	encodeMiniBlockInt32Default(&dst[0], src, bitWidth)
 }
 
-func miniBlockPackInt32AVX2(dst *byte, src *[miniBlockSize]int32, bitWidth uint) {
+func encodeMiniBlockInt32AVX2(dst *byte, src *[miniBlockSize]int32, bitWidth uint) {
 	switch {
 	case bitWidth == 1:
-		miniBlockPackInt32x1bitAVX2(dst, src)
+		encodeMiniBlockInt32x1bitAVX2(dst, src)
 	case bitWidth == 2:
-		miniBlockPackInt32x2bitsAVX2(dst, src)
+		encodeMiniBlockInt32x2bitsAVX2(dst, src)
 	case bitWidth == 32:
-		miniBlockPackInt32x32bitsAVX2(dst, src)
+		encodeMiniBlockInt32x32bitsAVX2(dst, src)
 	case bitWidth <= 16:
-		miniBlockPackInt32x3to16bitsAVX2(dst, src, bitWidth)
+		encodeMiniBlockInt32x3to16bitsAVX2(dst, src, bitWidth)
 	default:
-		miniBlockPackInt32Default(dst, src, bitWidth)
+		encodeMiniBlockInt32Default(dst, src, bitWidth)
 	}
 }
 
@@ -94,7 +95,7 @@ func encodeInt32AVX2(dst []byte, src []int32) []byte {
 		for i, bitWidth := range bitWidths {
 			if bitWidth != 0 {
 				miniBlock := (*[miniBlockSize]int32)(block[i*miniBlockSize:])
-				miniBlockPackInt32AVX2(&dst[n], miniBlock, uint(bitWidth))
+				encodeMiniBlockInt32AVX2(&dst[n], miniBlock, uint(bitWidth))
 				n += (miniBlockSize * int(bitWidth)) / 8
 			}
 		}
@@ -118,31 +119,31 @@ func blockSubInt64AVX2(block *[blockSize]int64, value int64)
 func blockBitWidthsInt64AVX2(bitWidths *[numMiniBlocks]byte, block *[blockSize]int64)
 
 //go:noescape
-func miniBlockPackInt64Default(dst *byte, src *[miniBlockSize]int64, bitWidth uint)
+func encodeMiniBlockInt64Default(dst *byte, src *[miniBlockSize]int64, bitWidth uint)
 
 //go:noescape
-func miniBlockPackInt64x1bitAVX2(dst *byte, src *[miniBlockSize]int64)
+func encodeMiniBlockInt64x1bitAVX2(dst *byte, src *[miniBlockSize]int64)
 
 //go:noescape
-func miniBlockPackInt64x2bitsAVX2(dst *byte, src *[miniBlockSize]int64)
+func encodeMiniBlockInt64x2bitsAVX2(dst *byte, src *[miniBlockSize]int64)
 
 //go:noescape
-func miniBlockPackInt64x64bitsAVX2(dst *byte, src *[miniBlockSize]int64)
+func encodeMiniBlockInt64x64bitsAVX2(dst *byte, src *[miniBlockSize]int64)
 
-func miniBlockPackInt64(dst []byte, src *[miniBlockSize]int64, bitWidth uint) {
-	miniBlockPackInt64Default(&dst[0], src, bitWidth)
+func encodeMiniBlockInt64(dst []byte, src *[miniBlockSize]int64, bitWidth uint) {
+	encodeMiniBlockInt64Default(&dst[0], src, bitWidth)
 }
 
-func miniBlockPackInt64AVX2(dst *byte, src *[miniBlockSize]int64, bitWidth uint) {
+func encodeMiniBlockInt64AVX2(dst *byte, src *[miniBlockSize]int64, bitWidth uint) {
 	switch {
 	case bitWidth == 1:
-		miniBlockPackInt64x1bitAVX2(dst, src)
+		encodeMiniBlockInt64x1bitAVX2(dst, src)
 	case bitWidth == 2:
-		miniBlockPackInt64x2bitsAVX2(dst, src)
+		encodeMiniBlockInt64x2bitsAVX2(dst, src)
 	case bitWidth == 64:
-		miniBlockPackInt64x64bitsAVX2(dst, src)
+		encodeMiniBlockInt64x64bitsAVX2(dst, src)
 	default:
-		miniBlockPackInt64Default(dst, src, bitWidth)
+		encodeMiniBlockInt64Default(dst, src, bitWidth)
 	}
 }
 
@@ -181,7 +182,7 @@ func encodeInt64AVX2(dst []byte, src []int64) []byte {
 		for i, bitWidth := range bitWidths {
 			if bitWidth != 0 {
 				miniBlock := (*[miniBlockSize]int64)(block[i*miniBlockSize:])
-				miniBlockPackInt64AVX2(&dst[n], miniBlock, uint(bitWidth))
+				encodeMiniBlockInt64AVX2(&dst[n], miniBlock, uint(bitWidth))
 				n += (miniBlockSize * int(bitWidth)) / 8
 			}
 		}
@@ -191,3 +192,65 @@ func encodeInt64AVX2(dst []byte, src []int64) []byte {
 
 	return dst
 }
+
+//go:noescape
+func decodeBlockInt32Default(dst []int32, minDelta, lastValue int32) int32
+
+//go:noescape
+func decodeBlockInt32AVX2(dst []int32, minDelta, lastValue int32) int32
+
+func decodeBlockInt32(dst []int32, minDelta, lastValue int32) int32 {
+	switch {
+	case cpu.X86.HasAVX2:
+		return decodeBlockInt32AVX2(dst, minDelta, lastValue)
+	default:
+		return decodeBlockInt32Default(dst, minDelta, lastValue)
+	}
+}
+
+//go:noescape
+func decodeMiniBlockInt32Default(dst []int32, src []uint32, bitWidth uint)
+
+//go:noescape
+func decodeMiniBlockInt32x1to16bitsAVX2(dst []int32, src []uint32, bitWidth uint)
+
+//go:noescape
+func decodeMiniBlockInt32x17to26bitsAVX2(dst []int32, src []uint32, bitWidth uint)
+
+//go:noescape
+func decodeMiniBlockInt32x27to31bitsAVX2(dst []int32, src []uint32, bitWidth uint)
+
+func decodeMiniBlockInt32(dst []int32, src []uint32, bitWidth uint) {
+	hasAVX2 := cpu.X86.HasAVX2
+	switch {
+	case hasAVX2 && bitWidth <= 16:
+		decodeMiniBlockInt32x1to16bitsAVX2(dst, src, bitWidth)
+	case hasAVX2 && bitWidth <= 26:
+		decodeMiniBlockInt32x17to26bitsAVX2(dst, src, bitWidth)
+	case hasAVX2 && bitWidth <= 31:
+		decodeMiniBlockInt32x27to31bitsAVX2(dst, src, bitWidth)
+	case bitWidth == 32:
+		copy(dst, unsafecast.Uint32ToInt32(src))
+	default:
+		decodeMiniBlockInt32Default(dst, src, bitWidth)
+	}
+}
+
+//go:noescape
+func decodeBlockInt64Default(dst []int64, minDelta, lastValue int64) int64
+
+func decodeBlockInt64(dst []int64, minDelta, lastValue int64) int64 {
+	return decodeBlockInt64Default(dst, minDelta, lastValue)
+}
+
+//go:noescape
+func decodeMiniBlockInt64Default(dst []int64, src []uint32, bitWidth uint)
+
+func decodeMiniBlockInt64(dst []int64, src []uint32, bitWidth uint) {
+	switch {
+	case bitWidth == 64:
+		copy(dst, unsafecast.Uint32ToInt64(src))
+	default:
+		decodeMiniBlockInt64Default(dst, src, bitWidth)
+	}
+}
diff --git a/encoding/delta/binary_packed_amd64.s b/encoding/delta/binary_packed_amd64.s
index 830a2c5..08f80f0 100644
--- a/encoding/delta/binary_packed_amd64.s
+++ b/encoding/delta/binary_packed_amd64.s
@@ -19,13 +19,13 @@
     VPERMD Y1, Y3, Y0
 
 // func blockDeltaInt32AVX2(block *[blockSize]int32, lastValue int32) int32
-TEXT ·blockDeltaInt32AVX2(SB), NOSPLIT, $0-24
+TEXT ·blockDeltaInt32AVX2(SB), NOSPLIT, $0-20
     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
+    VMOVDQU ·rotateLeft32(SB), Y3
 
     XORQ SI, SI
 loop:
@@ -39,18 +39,8 @@ 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
+TEXT ·blockMinInt32AVX2(SB), NOSPLIT, $0-12
     MOVQ block+0(FP), AX
     VPBROADCASTD (AX), Y15
 
@@ -120,7 +110,7 @@ TEXT ·blockMinInt32AVX2(SB), NOSPLIT, $0-16
     VMOVDQU Y4, offset+96(baseAddr)
 
 // func blockSubInt32AVX2(block *[blockSize]int32, value int32)
-TEXT ·blockSubInt32AVX2(SB), NOSPLIT, $0-16
+TEXT ·blockSubInt32AVX2(SB), NOSPLIT, $0-12
     MOVQ block+0(FP), AX
     VPBROADCASTD value+8(FP), Y0
     subInt32AVX2x32(AX, 0)
@@ -187,15 +177,15 @@ loop:
     VZEROUPPER
     RET
 
-// miniBlockPackInt32Default is the generic implementation of the algorithm to
+// encodeMiniBlockInt32Default 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
+// func encodeMiniBlockInt32Default(dst *byte, src *[miniBlockSize]int32, bitWidth uint)
+TEXT ·encodeMiniBlockInt32Default(SB), NOSPLIT, $0-24
     MOVQ dst+0(FP), AX
     MOVQ src+8(FP), BX
     MOVQ bitWidth+16(FP), R9
@@ -219,7 +209,7 @@ loop:
     JNE loop
     RET
 
-// miniBlockPackInt32x1bitAVX2 packs 32 bit integers into 1 bit values in the
+// encodeMiniBlockInt32x1bitAVX2 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
@@ -228,8 +218,8 @@ loop:
 // 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
+// func encodeMiniBlockInt32x1bitAVX2(dst *byte, src *[miniBlockSize]int32)
+TEXT ·encodeMiniBlockInt32x1bitAVX2(SB), NOSPLIT, $0-16
     MOVQ dst+0(FP), AX
     MOVQ src+8(FP), BX
 
@@ -259,17 +249,17 @@ TEXT ·miniBlockPackInt32x1bitAVX2(SB), NOSPLIT, $0-16
     VZEROUPPER
     RET
 
-// miniBlockPackInt32x2bitsAVX2 implements an algorithm for packing 32 bit
+// encodeMiniBlockInt32x2bitsAVX2 implements an algorithm for packing 32 bit
 // integers into 2 bit values.
 //
-// The algorithm is derived from the one employed in miniBlockPackInt32x1bitAVX2
+// The algorithm is derived from the one employed in encodeMiniBlockInt32x1bitAVX2
 // 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
+// func encodeMiniBlockInt32x2bitsAVX2(dst *byte, src *[miniBlockSize]int32)
+TEXT ·encodeMiniBlockInt32x2bitsAVX2(SB), NOSPLIT, $0-16
     MOVQ dst+0(FP), AX
     MOVQ src+8(FP), BX
 
@@ -323,12 +313,12 @@ TEXT ·miniBlockPackInt32x2bitsAVX2(SB), NOSPLIT, $0-16
     VZEROUPPER
     RET
 
-// miniBlockPackInt32x32bitsAVX2 is a specialization of the bit packing logic
+// encodeMiniBlockInt32x32bitsAVX2 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
+// func encodeMiniBlockInt32x32bitsAVX2(dst *byte, src *[miniBlockSize]int32)
+TEXT ·encodeMiniBlockInt32x32bitsAVX2(SB), NOSPLIT, $0-16
     MOVQ dst+0(FP), AX
     MOVQ src+8(FP), BX
     VMOVDQU 0(BX), Y0
@@ -342,7 +332,7 @@ TEXT ·miniBlockPackInt32x32bitsAVX2(SB), NOSPLIT, $0-16
     VZEROUPPER
     RET
 
-// miniBlockPackInt32x3to16bitsAVX2 is the algorithm used to bit-pack 32 bit
+// encodeMiniBlockInt32x3to16bitsAVX2 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
@@ -369,7 +359,7 @@ TEXT ·miniBlockPackInt32x32bitsAVX2(SB), NOSPLIT, $0-16
 // 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
+TEXT ·encodeMiniBlockInt32x3to16bitsAVX2(SB), NOSPLIT, $0-24
     MOVQ dst+0(FP), AX
     MOVQ src+8(FP), BX
     MOVQ bitWidth+16(FP), CX
@@ -429,6 +419,84 @@ DATA sixtyfour<>+8(SB)/8, $64
 DATA sixtyfour<>+16(SB)/8, $64
 DATA sixtyfour<>+24(SB)/8, $64
 
+// func decodeBlockInt32Default(dst []int32, minDelta, lastValue int32) int32
+TEXT ·decodeBlockInt32Default(SB), NOSPLIT, $0-36
+    MOVQ dst_base+0(FP), AX
+    MOVQ dst_len+8(FP), BX
+    MOVLQZX minDelta+24(FP), CX
+    MOVLQZX lastValue+28(FP), DX
+    XORQ SI, SI
+    JMP test
+loop:
+    MOVL (AX)(SI*4), DI
+    ADDL CX, DI
+    ADDL DI, DX
+    MOVL DX, (AX)(SI*4)
+    INCQ SI
+test:
+    CMPQ SI, BX
+    JNE loop
+done:
+    MOVL DX, ret+32(FP)
+    RET
+
+// func decodeBlockInt32AVX2(dst []int32, minDelta, lastValue int32) int32
+TEXT ·decodeBlockInt32AVX2(SB), NOSPLIT, $0-36
+    MOVQ dst_base+0(FP), AX
+    MOVQ dst_len+8(FP), BX
+    MOVLQZX minDelta+24(FP), CX
+    MOVLQZX lastValue+28(FP), DX
+    XORQ SI, SI
+
+    CMPQ BX, $8
+    JB test
+
+    MOVQ BX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+
+    VPXOR X1, X1, X1
+    MOVQ CX, X0
+    MOVQ DX, X1
+    VPBROADCASTD X0, Y0
+loopAVX2:
+    VMOVDQU (AX)(SI*4), Y2
+    VPADDD Y0, Y2, Y2 // Y2[:] += minDelta
+    VPADDD Y1, Y2, Y2 // Y2[0] += lastValue
+
+    VPSLLDQ $4, Y2, Y3
+    VPADDD Y3, Y2, Y2
+
+    VPSLLDQ $8, Y2, Y3
+    VPADDD Y3, Y2, Y2
+
+    VPSHUFD $0xFF, X2, X1
+    VPERM2I128 $1, Y2, Y2, Y3
+    VPADDD X1, X3, X3
+
+    VMOVDQU X2, (AX)(SI*4)
+    VMOVDQU X3, 16(AX)(SI*4)
+    VPSRLDQ $12, X3, X1 // lastValue
+
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loopAVX2
+    VZEROUPPER
+    MOVQ X1, DX
+    JMP test
+loop:
+    MOVL (AX)(SI*4), DI
+    ADDL CX, DI
+    ADDL DI, DX
+    MOVL DX, (AX)(SI*4)
+    INCQ SI
+test:
+    CMPQ SI, BX
+    JNE loop
+done:
+    MOVL DX, ret+32(FP)
+    RET
+
 // -----------------------------------------------------------------------------
 // 64 bits
 // -----------------------------------------------------------------------------
@@ -622,15 +690,15 @@ loop:
     VZEROUPPER
     RET
 
-// miniBlockPackInt64Default is the generic implementation of the algorithm to
+// encodeMiniBlockInt64Default 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
+// func encodeMiniBlockInt64Default(dst *byte, src *[miniBlockSize]int64, bitWidth uint)
+TEXT ·encodeMiniBlockInt64Default(SB), NOSPLIT, $0-24
     MOVQ dst+0(FP), AX
     MOVQ src+8(FP), BX
     MOVQ bitWidth+16(FP), R10
@@ -663,8 +731,8 @@ loop:
     JNE loop
     RET
 
-// func miniBlockPackInt64x1bitAVX2(dst *byte, src *[miniBlockSize]int64)
-TEXT ·miniBlockPackInt64x1bitAVX2(SB), NOSPLIT, $0-16
+// func encodeMiniBlockInt64x1bitAVX2(dst *byte, src *[miniBlockSize]int64)
+TEXT ·encodeMiniBlockInt64x1bitAVX2(SB), NOSPLIT, $0-16
     MOVQ dst+0(FP), AX
     MOVQ src+8(FP), BX
 
@@ -715,8 +783,8 @@ TEXT ·miniBlockPackInt64x1bitAVX2(SB), NOSPLIT, $0-16
     VZEROUPPER
     RET
 
-// func miniBlockPackInt64x2bitsAVX2(dst *byte, src *[miniBlockSize]int64)
-TEXT ·miniBlockPackInt64x2bitsAVX2(SB), NOSPLIT, $0-16
+// func encodeMiniBlockInt64x2bitsAVX2(dst *byte, src *[miniBlockSize]int64)
+TEXT ·encodeMiniBlockInt64x2bitsAVX2(SB), NOSPLIT, $0-16
     MOVQ dst+0(FP), AX
     MOVQ src+8(FP), BX
 
@@ -807,8 +875,8 @@ TEXT ·miniBlockPackInt64x2bitsAVX2(SB), NOSPLIT, $0-16
     VZEROUPPER
     RET
 
-// func miniBlockPackInt64x64bitsAVX2(dst *byte, src *[miniBlockSize]int64)
-TEXT ·miniBlockPackInt64x64bitsAVX2(SB), NOSPLIT, $0-16
+// func encodeMiniBlockInt64x64bitsAVX2(dst *byte, src *[miniBlockSize]int64)
+TEXT ·encodeMiniBlockInt64x64bitsAVX2(SB), NOSPLIT, $0-16
     MOVQ dst+0(FP), AX
     MOVQ src+8(FP), BX
     VMOVDQU 0(BX), Y0
@@ -829,3 +897,24 @@ TEXT ·miniBlockPackInt64x64bitsAVX2(SB), NOSPLIT, $0-16
     VMOVDQU Y7, 224(AX)
     VZEROUPPER
     RET
+
+// func decodeBlockInt64Default(dst []int64, minDelta, lastValue int64) int64
+TEXT ·decodeBlockInt64Default(SB), NOSPLIT, $0-48
+    MOVQ dst_base+0(FP), AX
+    MOVQ dst_len+8(FP), BX
+    MOVQ minDelta+24(FP), CX
+    MOVQ lastValue+32(FP), DX
+    XORQ SI, SI
+    JMP test
+loop:
+    MOVQ (AX)(SI*8), DI
+    ADDQ CX, DI
+    ADDQ DI, DX
+    MOVQ DX, (AX)(SI*8)
+    INCQ SI
+test:
+    CMPQ SI, BX
+    JNE loop
+done:
+    MOVQ DX, ret+40(FP)
+    RET
diff --git a/encoding/delta/binary_packed_purego.go b/encoding/delta/binary_packed_purego.go
index 66d24d9..aa1f623 100644
--- a/encoding/delta/binary_packed_purego.go
+++ b/encoding/delta/binary_packed_purego.go
@@ -6,7 +6,7 @@ import (
 	"encoding/binary"
 )
 
-func miniBlockPackInt32(dst []byte, src *[miniBlockSize]int32, bitWidth uint) {
+func encodeMiniBlockInt32(dst []byte, src *[miniBlockSize]int32, bitWidth uint) {
 	bitMask := uint32(1<<bitWidth) - 1
 	bitOffset := uint(0)
 
@@ -27,7 +27,7 @@ func miniBlockPackInt32(dst []byte, src *[miniBlockSize]int32, bitWidth uint) {
 	}
 }
 
-func miniBlockPackInt64(dst []byte, src *[miniBlockSize]int64, bitWidth uint) {
+func encodeMiniBlockInt64(dst []byte, src *[miniBlockSize]int64, bitWidth uint) {
 	bitMask := uint64(1<<bitWidth) - 1
 	bitOffset := uint(0)
 
@@ -47,3 +47,59 @@ func miniBlockPackInt64(dst []byte, src *[miniBlockSize]int64, bitWidth uint) {
 		bitOffset += bitWidth
 	}
 }
+
+func decodeBlockInt32(block []int32, minDelta, lastValue int32) int32 {
+	for i := range block {
+		block[i] += minDelta
+		block[i] += lastValue
+		lastValue = block[i]
+	}
+	return lastValue
+}
+
+func decodeBlockInt64(block []int64, minDelta, lastValue int64) int64 {
+	for i := range block {
+		block[i] += minDelta
+		block[i] += lastValue
+		lastValue = block[i]
+	}
+	return lastValue
+}
+
+func decodeMiniBlockInt32(dst []int32, src []uint32, bitWidth uint) {
+	bitMask := uint32(1<<bitWidth) - 1
+	bitOffset := uint(0)
+
+	for n := range dst {
+		i := bitOffset / 32
+		j := bitOffset % 32
+		d := (src[i] & (bitMask << j)) >> j
+		if j+bitWidth > 32 {
+			k := 32 - j
+			d |= (src[i+1] & (bitMask >> k)) << k
+		}
+		dst[n] = int32(d)
+		bitOffset += bitWidth
+	}
+}
+
+func decodeMiniBlockInt64(dst []int64, src []uint32, bitWidth uint) {
+	bitMask := uint64(1<<bitWidth) - 1
+	bitOffset := uint(0)
+
+	for n := range dst {
+		i := bitOffset / 32
+		j := bitOffset % 32
+		d := (uint64(src[i]) & (bitMask << j)) >> j
+		if j+bitWidth > 32 {
+			k := 32 - j
+			d |= (uint64(src[i+1]) & (bitMask >> k)) << k
+			if j+bitWidth > 64 {
+				k := 64 - j
+				d |= (uint64(src[i+2]) & (bitMask >> k)) << k
+			}
+		}
+		dst[n] = int64(d)
+		bitOffset += bitWidth
+	}
+}
diff --git a/encoding/delta/byte_array.go b/encoding/delta/byte_array.go
index ed34c1a..38ba7ad 100644
--- a/encoding/delta/byte_array.go
+++ b/encoding/delta/byte_array.go
@@ -137,90 +137,54 @@ func (e *ByteArrayEncoding) EncodeFixedLenByteArray(dst, src []byte, size int) (
 
 func (e *ByteArrayEncoding) DecodeByteArray(dst, src []byte) ([]byte, error) {
 	dst = dst[:0]
-	err := e.decode(src, func(prefix, suffix []byte) ([]byte, error) {
-		n := len(prefix) + len(suffix)
-		b := [4]byte{}
-		plain.PutByteArrayLength(b[:], n)
-		dst = append(dst, b[:]...)
-		i := len(dst)
-		dst = append(dst, prefix...)
-		dst = append(dst, suffix...)
-		return dst[i:len(dst):len(dst)], nil
-	})
+
+	prefix := getInt32Buffer()
+	defer putInt32Buffer(prefix)
+
+	suffix := getInt32Buffer()
+	defer putInt32Buffer(suffix)
+
+	var err error
+	src, err = prefix.decode(src)
 	if err != nil {
-		err = encoding.Error(e, err)
+		return dst, encoding.Errorf(e, "decoding prefix lengths: %w", err)
 	}
-	return dst, err
+	src, err = suffix.decode(src)
+	if err != nil {
+		return dst, encoding.Errorf(e, "decoding suffix lengths: %w", err)
+	}
+	if len(prefix.values) != len(suffix.values) {
+		return dst, encoding.Error(e, errPrefixAndSuffixLengthMismatch(len(prefix.values), len(suffix.values)))
+	}
+	return decodeByteArray(dst, src, prefix.values, suffix.values)
 }
 
 func (e *ByteArrayEncoding) DecodeFixedLenByteArray(dst, src []byte, size int) ([]byte, error) {
-	if size < 0 || size > encoding.MaxFixedLenByteArraySize {
-		return dst[:0], encoding.Error(e, encoding.ErrInvalidArgument)
-	}
 	dst = dst[:0]
-	err := e.decode(src, func(prefix, suffix []byte) ([]byte, error) {
-		n := len(prefix) + len(suffix)
-		if n != size {
-			return nil, fmt.Errorf("cannot decode value of size %d into fixed-length byte array of size %d", n, size)
-		}
-		i := len(dst)
-		dst = append(dst, prefix...)
-		dst = append(dst, suffix...)
-		return dst[i:len(dst):len(dst)], nil
-	})
-	if err != nil {
-		err = encoding.Error(e, err)
+
+	if size < 0 || size > encoding.MaxFixedLenByteArraySize {
+		return dst, encoding.Error(e, encoding.ErrInvalidArgument)
 	}
-	return dst, err
-}
 
-func (e *ByteArrayEncoding) decode(src []byte, observe func(prefix, suffix []byte) ([]byte, error)) error {
 	prefix := getInt32Buffer()
 	defer putInt32Buffer(prefix)
 
-	length := getInt32Buffer()
-	defer putInt32Buffer(length)
+	suffix := getInt32Buffer()
+	defer putInt32Buffer(suffix)
 
 	var err error
 	src, err = prefix.decode(src)
 	if err != nil {
-		return err
+		return dst, fmt.Errorf("decoding prefix lengths: %w", err)
 	}
-	src, err = length.decode(src)
+	src, err = suffix.decode(src)
 	if err != nil {
-		return err
-	}
-	if len(prefix.values) != len(length.values) {
-		return fmt.Errorf("number of prefix and lengths mismatch: %d != %d", len(prefix.values), len(length.values))
+		return dst, fmt.Errorf("decoding suffix lengths: %w", err)
 	}
-
-	var lastValue []byte
-	for i, n := range length.values {
-		if int(n) < 0 {
-			return fmt.Errorf("invalid negative value length: %d", n)
-		}
-		if int(n) > len(src) {
-			return fmt.Errorf("value length is larger than the input size: %d > %d", n, len(src))
-		}
-
-		p := prefix.values[i]
-		if int(p) < 0 {
-			return fmt.Errorf("invalid negative prefix length: %d", p)
-		}
-		if int(p) > len(lastValue) {
-			return fmt.Errorf("prefix length %d is larger than the last value of size %d", p, len(lastValue))
-		}
-
-		prefix := lastValue[:p:p]
-		suffix := src[:n:n]
-		src = src[n:len(src):len(src)]
-
-		if lastValue, err = observe(prefix, suffix); err != nil {
-			return err
-		}
+	if len(prefix.values) != len(suffix.values) {
+		return dst, errPrefixAndSuffixLengthMismatch(len(prefix.values), len(suffix.values))
 	}
-
-	return nil
+	return decodeFixedLenByteArray(dst, src, size, prefix.values, suffix.values)
 }
 
 func linearSearchPrefixLength(base, data []byte) (n int) {
diff --git a/encoding/delta/byte_array_amd64.go b/encoding/delta/byte_array_amd64.go
new file mode 100644
index 0000000..f05b02c
--- /dev/null
+++ b/encoding/delta/byte_array_amd64.go
@@ -0,0 +1,160 @@
+//go:build !purego
+
+package delta
+
+import (
+	"github.com/segmentio/parquet-go/encoding/plain"
+	"golang.org/x/sys/cpu"
+)
+
+//go:noescape
+func validatePrefixAndSuffixLengthValuesAVX2(prefix, suffix []int32, maxLength int) (totalPrefixLength, totalSuffixLength int, ok bool)
+
+func validatePrefixAndSuffixLengthValues(prefix, suffix []int32, maxLength int) (totalPrefixLength, totalSuffixLength int, err error) {
+	if cpu.X86.HasAVX2 {
+		totalPrefixLength, totalSuffixLength, ok := validatePrefixAndSuffixLengthValuesAVX2(prefix, suffix, maxLength)
+		if ok {
+			return totalPrefixLength, totalSuffixLength, nil
+		}
+	}
+
+	lastValueLength := 0
+
+	for i := range prefix {
+		p := int(prefix[i])
+		n := int(suffix[i])
+		if p < 0 {
+			err = errInvalidNegativePrefixLength(p)
+			return
+		}
+		if n < 0 {
+			err = errInvalidNegativeValueLength(n)
+			return
+		}
+		if p > lastValueLength {
+			err = errPrefixLengthOutOfBounds(p, lastValueLength)
+			return
+		}
+		totalPrefixLength += p
+		totalSuffixLength += n
+		lastValueLength = p + n
+	}
+
+	if totalSuffixLength > maxLength {
+		err = errValueLengthOutOfBounds(totalSuffixLength, maxLength)
+		return
+	}
+
+	return totalPrefixLength, totalSuffixLength, nil
+}
+
+//go:noescape
+func decodeByteArrayAVX2(dst, src []byte, prefix, suffix []int32) int
+
+func decodeByteArray(dst, src []byte, prefix, suffix []int32) ([]byte, error) {
+	totalPrefixLength, totalSuffixLength, err := validatePrefixAndSuffixLengthValues(prefix, suffix, len(src))
+	if err != nil {
+		return dst, err
+	}
+
+	totalLength := plain.ByteArrayLengthSize*len(prefix) + totalPrefixLength + totalSuffixLength
+	dst = resizeNoMemclr(dst, totalLength+padding)
+
+	_ = prefix[:len(suffix)]
+	_ = suffix[:len(prefix)]
+
+	var lastValue []byte
+	var i int
+	var j int
+
+	if cpu.X86.HasAVX2 && len(src) > padding {
+		k := len(suffix)
+		n := 0
+
+		for k > 0 && n < padding {
+			k--
+			n += int(suffix[k])
+		}
+
+		if k > 0 && n >= padding {
+			i = decodeByteArrayAVX2(dst, src, prefix[:k], suffix[:k])
+			j = len(src) - n
+			lastValue = dst[i-(int(prefix[k-1])+int(suffix[k-1])):]
+			prefix = prefix[k:]
+			suffix = suffix[k:]
+		}
+	}
+
+	for k := range prefix {
+		p := int(prefix[k])
+		n := int(suffix[k])
+		plain.PutByteArrayLength(dst[i:], p+n)
+		i += plain.ByteArrayLengthSize
+		k := i
+		i += copy(dst[i:], lastValue[:p])
+		i += copy(dst[i:], src[j:j+n])
+		j += n
+		lastValue = dst[k:]
+	}
+
+	return dst[:totalLength], nil
+}
+
+//go:noescape
+func decodeFixedLenByteArrayAVX2(dst, src []byte, prefix, suffix []int32) int
+
+//go:noescape
+func decodeFixedLenByteArrayAVX2x128bits(dst, src []byte, prefix, suffix []int32) int
+
+func decodeFixedLenByteArray(dst, src []byte, size int, prefix, suffix []int32) ([]byte, error) {
+	totalPrefixLength, totalSuffixLength, err := validatePrefixAndSuffixLengthValues(prefix, suffix, len(src))
+	if err != nil {
+		return dst, err
+	}
+
+	totalLength := totalPrefixLength + totalSuffixLength
+	dst = resizeNoMemclr(dst, totalLength+padding)
+
+	_ = prefix[:len(suffix)]
+	_ = suffix[:len(prefix)]
+
+	var lastValue []byte
+	var i int
+	var j int
+
+	if cpu.X86.HasAVX2 && len(src) > padding {
+		k := len(suffix)
+		n := 0
+
+		for k > 0 && n < padding {
+			k--
+			n += int(suffix[k])
+		}
+
+		if k > 0 && n >= padding {
+			if size == 16 {
+				i = decodeFixedLenByteArrayAVX2x128bits(dst, src, prefix[:k], suffix[:k])
+			} else {
+				i = decodeFixedLenByteArrayAVX2(dst, src, prefix[:k], suffix[:k])
+			}
+			j = len(src) - n
+			prefix = prefix[k:]
+			suffix = suffix[k:]
+			if i >= size {
+				lastValue = dst[i-size:]
+			}
+		}
+	}
+
+	for k := range prefix {
+		p := int(prefix[k])
+		n := int(suffix[k])
+		k := i
+		i += copy(dst[i:], lastValue[:p])
+		i += copy(dst[i:], src[j:j+n])
+		j += n
+		lastValue = dst[k:]
+	}
+
+	return dst[:totalLength], nil
+}
diff --git a/encoding/delta/byte_array_amd64.s b/encoding/delta/byte_array_amd64.s
new file mode 100644
index 0000000..dec5f0d
--- /dev/null
+++ b/encoding/delta/byte_array_amd64.s
@@ -0,0 +1,285 @@
+//go:build !purego
+
+#include "funcdata.h"
+#include "textflag.h"
+
+// func validatePrefixAndSuffixLengthValuesAVX2(prefix, suffix []int32, maxLength int) (totalPrefixLength, totalSuffixLength int, ok bool)
+TEXT ·validatePrefixAndSuffixLengthValuesAVX2(SB), NOSPLIT, $0-73
+    MOVQ prefix_base+0(FP), AX
+    MOVQ suffix_base+24(FP), BX
+    MOVQ suffix_len+32(FP), CX
+    MOVQ maxLength+48(FP), DX
+
+    XORQ SI, SI
+    XORQ DI, DI // lastValueLength
+    XORQ R8, R8
+    XORQ R9, R9
+    XORQ R10, R10 // totalPrefixLength
+    XORQ R11, R11 // totalSuffixLength
+    XORQ R12, R12 // ok
+
+    CMPQ CX, $8
+    JB test
+
+    MOVQ CX, R13
+    SHRQ $3, R13
+    SHLQ $3, R13
+
+    VPXOR X0, X0, X0 // lastValueLengths
+    VPXOR X1, X1, X1 // totalPrefixLengths
+    VPXOR X2, X2, X2 // totalSuffixLengths
+    VPXOR X3, X3, X3 // negative prefix length sentinels
+    VPXOR X4, X4, X4 // negative suffix length sentinels
+    VPXOR X5, X5, X5 // prefix length overflow sentinels
+    VMOVDQU ·rotateLeft32(SB), Y6
+
+loopAVX2:
+    VMOVDQU (AX)(SI*4), Y7 // p
+    VMOVDQU (BX)(SI*4), Y8 // n
+
+    VPADDD Y7, Y1, Y1
+    VPADDD Y8, Y2, Y2
+
+    VPOR Y7, Y3, Y3
+    VPOR Y8, Y4, Y4
+
+    VPADDD Y7, Y8, Y9 // p + n
+    VPERMD Y0, Y6, Y10
+    VPBLENDD $1, Y10, Y9, Y10
+    VPCMPGTD Y10, Y7, Y10
+    VPOR Y10, Y5, Y5
+
+    VMOVDQU Y9, Y0
+    ADDQ $8, SI
+    CMPQ SI, R13
+    JNE loopAVX2
+
+    // If any of the sentinel values has its most significant bit set then one
+    // of the values was negative or one of the prefixes was greater than the
+    // length of the previous value, return false.
+    VPOR Y4, Y3, Y3
+    VPOR Y5, Y3, Y3
+    VMOVMSKPS Y3, R13
+    CMPQ R13, $0
+    JNE done
+
+    // We computed 8 sums in parallel for the prefix and suffix arrays, they
+    // need to be accumulated into single values, which is what these reduction
+    // steps do.
+    VPSRLDQ $4, Y1, Y5
+    VPSRLDQ $8, Y1, Y6
+    VPSRLDQ $12, Y1, Y7
+    VPADDD Y5, Y1, Y1
+    VPADDD Y6, Y1, Y1
+    VPADDD Y7, Y1, Y1
+    VPERM2I128 $1, Y1, Y1, Y0
+    VPADDD Y0, Y1, Y1
+    MOVQ X1, R10
+    ANDQ $0x7FFFFFFF, R10
+
+    VPSRLDQ $4, Y2, Y5
+    VPSRLDQ $8, Y2, Y6
+    VPSRLDQ $12, Y2, Y7
+    VPADDD Y5, Y2, Y2
+    VPADDD Y6, Y2, Y2
+    VPADDD Y7, Y2, Y2
+    VPERM2I128 $1, Y2, Y2, Y0
+    VPADDD Y0, Y2, Y2
+    MOVQ X2, R11
+    ANDQ $0x7FFFFFFF, R11
+
+    JMP test
+loop:
+    MOVLQSX (AX)(SI*4), R8
+    MOVLQSX (BX)(SI*4), R9
+
+    CMPQ R8, $0 // p < 0 ?
+    JL done
+
+    CMPQ R9, $0 // n < 0 ?
+    JL done
+
+    CMPQ R8, DI // p > lastValueLength ?
+    JG done
+
+    ADDQ R8, R10
+    ADDQ R9, R11
+    ADDQ R8, DI
+    ADDQ R9, DI
+
+    INCQ SI
+test:
+    CMPQ SI, CX
+    JNE loop
+
+    CMPQ R11, DX // totalSuffixLength > maxLength ?
+    JG done
+
+    MOVB $1, R12
+done:
+    MOVQ R10, totalPrefixLength+56(FP)
+    MOVQ R11, totalSuffixLength+64(FP)
+    MOVB R12, ok+72(FP)
+    RET
+
+// func decodeByteArrayAVX2(dst, src []byte, prefix, suffix []int32) int
+TEXT ·decodeByteArrayAVX2(SB), NOSPLIT, $0-104
+    MOVQ dst_base+0(FP), AX
+    MOVQ src_base+24(FP), BX
+    MOVQ prefix_base+48(FP), CX
+    MOVQ suffix_base+72(FP), DX
+    MOVQ suffix_len+80(FP), DI
+
+    ADDQ $4, AX
+    XORQ SI, SI
+    XORQ R8, R8
+    XORQ R9, R9
+    MOVQ AX, R10 // last value
+
+    JMP test
+loop:
+    MOVLQZX (CX)(SI*4), R8 // prefix length
+    MOVLQZX (DX)(SI*4), R9 // suffix length
+    MOVQ R8, R11
+    ADDQ R9, R11
+    MOVL R11, -4(AX)
+prefix:
+    VMOVDQU (R10), X0
+    VMOVDQU X0, (AX)
+    CMPQ R8, $16
+    JA copyPrefix
+suffix:
+    VMOVDQU (BX), X1
+    VMOVDQU X1, (AX)(R8*1)
+    CMPQ R9, $16
+    JA copySuffix
+next:
+    MOVQ AX, R10
+    LEAQ 4(AX)(R11*1), AX
+    LEAQ 0(BX)(R9*1), BX
+    INCQ SI
+test:
+    CMPQ SI, DI
+    JNE loop
+    MOVQ dst_base+0(FP), BX
+    SUBQ BX, AX
+    SUBQ $4, AX
+    MOVQ AX, ret+96(FP)
+    VZEROUPPER
+    RET
+copyPrefix:
+    MOVQ $16, R12
+copyPrefixLoop:
+    VMOVDQU (R10)(R12*1), Y0
+    VMOVDQU Y0, (AX)(R12*1)
+    ADDQ $32, R12
+    CMPQ R12, R8
+    JB copyPrefixLoop
+    JMP suffix
+copySuffix:
+    MOVQ $16, R12
+    LEAQ (AX)(R8*1), R13
+copySuffixLoop:
+    VMOVDQU (BX)(R12*1), Y1
+    VMOVDQU Y1, (R13)(R12*1)
+    ADDQ $32, R12
+    CMPQ R12, R9
+    JB copySuffixLoop
+    JMP next
+
+// func decodeFixedLenByteArrayAVX2(dst, src []byte, prefix, suffix []int32) int
+TEXT ·decodeFixedLenByteArrayAVX2(SB), NOSPLIT, $0-104
+    MOVQ dst_base+0(FP), AX
+    MOVQ src_base+24(FP), BX
+    MOVQ prefix_base+48(FP), CX
+    MOVQ suffix_base+72(FP), DX
+    MOVQ suffix_len+80(FP), DI
+
+    XORQ SI, SI
+    XORQ R8, R8
+    XORQ R9, R9
+    MOVQ AX, R10 // last value
+
+    JMP test
+loop:
+    MOVLQZX (CX)(SI*4), R8 // prefix length
+    MOVLQZX (DX)(SI*4), R9 // suffix length
+prefix:
+    VMOVDQU (R10), Y0
+    VMOVDQU Y0, (AX)
+    CMPQ R8, $32
+    JA copyPrefix
+suffix:
+    VMOVDQU (BX), Y1
+    VMOVDQU Y1, (AX)(R8*1)
+    CMPQ R9, $32
+    JA copySuffix
+next:
+    MOVQ AX, R10
+    ADDQ R9, R8
+    LEAQ (AX)(R8*1), AX
+    LEAQ (BX)(R9*1), BX
+    INCQ SI
+test:
+    CMPQ SI, DI
+    JNE loop
+    MOVQ dst_base+0(FP), BX
+    SUBQ BX, AX
+    MOVQ AX, ret+96(FP)
+    VZEROUPPER
+    RET
+copyPrefix:
+    MOVQ $32, R12
+copyPrefixLoop:
+    VMOVDQU (R10)(R12*1), Y0
+    VMOVDQU Y0, (AX)(R12*1)
+    ADDQ $32, R12
+    CMPQ R12, R8
+    JB copyPrefixLoop
+    JMP suffix
+copySuffix:
+    MOVQ $32, R12
+    LEAQ (AX)(R8*1), R13
+copySuffixLoop:
+    VMOVDQU (BX)(R12*1), Y1
+    VMOVDQU Y1, (R13)(R12*1)
+    ADDQ $32, R12
+    CMPQ R12, R9
+    JB copySuffixLoop
+    JMP next
+
+// func decodeFixedLenByteArrayAVX2x128bits(dst, src []byte, prefix, suffix []int32) int
+TEXT ·decodeFixedLenByteArrayAVX2x128bits(SB), NOSPLIT, $0-104
+    MOVQ dst_base+0(FP), AX
+    MOVQ src_base+24(FP), BX
+    MOVQ prefix_base+48(FP), CX
+    MOVQ suffix_base+72(FP), DX
+    MOVQ suffix_len+80(FP), DI
+
+    XORQ SI, SI
+    XORQ R8, R8
+    XORQ R9, R9
+    VPXOR X0, X0, X0
+
+    JMP test
+loop:
+    MOVLQZX (CX)(SI*4), R8 // prefix length
+    MOVLQZX (DX)(SI*4), R9 // suffix length
+
+    VMOVDQU (BX), X1
+    VMOVDQU X0, (AX)
+    VMOVDQU X1, (AX)(R8*1)
+    VMOVDQU (AX), X0
+
+    ADDQ R9, R8
+    LEAQ (AX)(R8*1), AX
+    LEAQ (BX)(R9*1), BX
+    INCQ SI
+test:
+    CMPQ SI, DI
+    JNE loop
+    MOVQ dst_base+0(FP), BX
+    SUBQ BX, AX
+    MOVQ AX, ret+96(FP)
+    VZEROUPPER
+    RET
diff --git a/encoding/delta/byte_array_purego.go b/encoding/delta/byte_array_purego.go
new file mode 100644
index 0000000..d975181
--- /dev/null
+++ b/encoding/delta/byte_array_purego.go
@@ -0,0 +1,66 @@
+//go:build purego || !amd64
+
+package delta
+
+import (
+	"github.com/segmentio/parquet-go/encoding/plain"
+)
+
+func decodeByteArray(dst, src []byte, prefix, suffix []int32) ([]byte, error) {
+	_ = prefix[:len(suffix)]
+	_ = suffix[:len(prefix)]
+
+	var lastValue []byte
+	for i := range suffix {
+		n := int(suffix[i])
+		p := int(prefix[i])
+		if n < 0 {
+			return dst, errInvalidNegativeValueLength(n)
+		}
+		if n > len(src) {
+			return dst, errValueLengthOutOfBounds(n, len(src))
+		}
+		if p < 0 {
+			return dst, errInvalidNegativePrefixLength(p)
+		}
+		if p > len(lastValue) {
+			return dst, errPrefixLengthOutOfBounds(p, len(lastValue))
+		}
+		dst = plain.AppendByteArrayLength(dst, p+n)
+		j := len(dst)
+		dst = append(dst, lastValue[:p]...)
+		dst = append(dst, src[:n]...)
+		lastValue = dst[j:]
+		src = src[n:]
+	}
+	return dst, nil
+}
+
+func decodeFixedLenByteArray(dst, src []byte, size int, prefix, suffix []int32) ([]byte, error) {
+	_ = prefix[:len(suffix)]
+	_ = suffix[:len(prefix)]
+
+	var lastValue []byte
+	for i := range suffix {
+		n := int(suffix[i])
+		p := int(prefix[i])
+		if n < 0 {
+			return dst, errInvalidNegativeValueLength(n)
+		}
+		if n > len(src) {
+			return dst, errValueLengthOutOfBounds(n, len(src))
+		}
+		if p < 0 {
+			return dst, errInvalidNegativePrefixLength(p)
+		}
+		if p > len(lastValue) {
+			return dst, errPrefixLengthOutOfBounds(p, len(lastValue))
+		}
+		j := len(dst)
+		dst = append(dst, lastValue[:p]...)
+		dst = append(dst, src[:n]...)
+		lastValue = dst[j:]
+		src = src[n:]
+	}
+	return dst, nil
+}
diff --git a/encoding/delta/delta.go b/encoding/delta/delta.go
index 25f235b..6d27f0f 100644
--- a/encoding/delta/delta.go
+++ b/encoding/delta/delta.go
@@ -1,8 +1,11 @@
 package delta
 
 import (
+	"fmt"
 	"sync"
 	"unsafe"
+
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 type int32Buffer struct {
@@ -10,8 +13,9 @@ type int32Buffer struct {
 }
 
 func (buf *int32Buffer) decode(src []byte) ([]byte, error) {
-	var binpack BinaryPackedEncoding
-	return binpack.decode(src, func(value int64) { buf.values = append(buf.values, int32(value)) })
+	values, remain, err := decodeInt32(unsafecast.Int32ToBytes(buf.values[:0]), src)
+	buf.values = unsafecast.BytesToInt32(values)
+	return remain, err
 }
 
 var (
@@ -41,3 +45,60 @@ func bytesToInt32(b []byte) []int32 {
 func bytesToInt64(b []byte) []int64 {
 	return unsafe.Slice(*(**int64)(unsafe.Pointer(&b)), len(b)/8)
 }
+
+func resizeNoMemclr(buf []byte, size int) []byte {
+	if cap(buf) < size {
+		return grow(buf, size)
+	}
+	return buf[:size]
+}
+
+func resize(buf []byte, size int) []byte {
+	if cap(buf) < size {
+		return grow(buf, size)
+	}
+	if size > len(buf) {
+		clear := buf[len(buf):size]
+		for i := range clear {
+			clear[i] = 0
+		}
+	}
+	return buf[:size]
+}
+
+func grow(buf []byte, size int) []byte {
+	newCap := 2 * cap(buf)
+	if newCap < size {
+		newCap = size
+	}
+	newBuf := make([]byte, size, newCap)
+	copy(newBuf, buf)
+	return newBuf
+}
+
+func min(a, b int) int {
+	if a < b {
+		return a
+	}
+	return b
+}
+
+func errPrefixAndSuffixLengthMismatch(prefixLength, suffixLength int) error {
+	return fmt.Errorf("length of prefix and suffix mismatch: %d != %d", prefixLength, suffixLength)
+}
+
+func errInvalidNegativeValueLength(length int) error {
+	return fmt.Errorf("invalid negative value length: %d", length)
+}
+
+func errInvalidNegativePrefixLength(length int) error {
+	return fmt.Errorf("invalid negative prefix length: %d", length)
+}
+
+func errValueLengthOutOfBounds(length, maxLength int) error {
+	return fmt.Errorf("value length is larger than the input size: %d > %d", length, maxLength)
+}
+
+func errPrefixLengthOutOfBounds(length, maxLength int) error {
+	return fmt.Errorf("prefix length %d is larger than the last value of size %d", length, maxLength)
+}
diff --git a/encoding/delta/delta_amd64.go b/encoding/delta/delta_amd64.go
new file mode 100644
index 0000000..864aeac
--- /dev/null
+++ b/encoding/delta/delta_amd64.go
@@ -0,0 +1,16 @@
+//go:build !purego
+
+package delta
+
+const (
+	padding = 64
+)
+
+func findNegativeLength(lengths []int32) int {
+	for _, n := range lengths {
+		if n < 0 {
+			return int(n)
+		}
+	}
+	return -1
+}
diff --git a/encoding/delta/delta_amd64.s b/encoding/delta/delta_amd64.s
new file mode 100644
index 0000000..e8748a2
--- /dev/null
+++ b/encoding/delta/delta_amd64.s
@@ -0,0 +1,13 @@
+//go:build !purego
+
+#include "textflag.h"
+
+GLOBL ·rotateLeft32(SB), RODATA|NOPTR, $32
+DATA ·rotateLeft32+0(SB)/4, $7
+DATA ·rotateLeft32+4(SB)/4, $0
+DATA ·rotateLeft32+8(SB)/4, $1
+DATA ·rotateLeft32+12(SB)/4, $2
+DATA ·rotateLeft32+16(SB)/4, $3
+DATA ·rotateLeft32+20(SB)/4, $4
+DATA ·rotateLeft32+24(SB)/4, $5
+DATA ·rotateLeft32+28(SB)/4, $6
diff --git a/encoding/delta/length_byte_array.go b/encoding/delta/length_byte_array.go
index bddd3ec..639e28f 100644
--- a/encoding/delta/length_byte_array.go
+++ b/encoding/delta/length_byte_array.go
@@ -19,14 +19,8 @@ func (e *LengthByteArrayEncoding) Encoding() format.Encoding {
 }
 
 func (e *LengthByteArrayEncoding) EncodeByteArray(dst, src []byte) ([]byte, error) {
-	dst, err := e.encodeByteArray(dst[:0], src)
-	if err != nil {
-		err = encoding.Error(e, err)
-	}
-	return dst, err
-}
+	dst = dst[:0]
 
-func (e *LengthByteArrayEncoding) encodeByteArray(dst, src []byte) ([]byte, error) {
 	length := getInt32Buffer()
 	defer putInt32Buffer(length)
 
@@ -35,16 +29,16 @@ func (e *LengthByteArrayEncoding) encodeByteArray(dst, src []byte) ([]byte, erro
 	for i := 0; i < len(src); {
 		r := len(src) - i
 		if r < plain.ByteArrayLengthSize {
-			return dst, plain.ErrTooShort(r)
+			return dst, encoding.Error(e, plain.ErrTooShort(r))
 		}
 		n := plain.ByteArrayLength(src[i:])
 		i += plain.ByteArrayLengthSize
 		r -= plain.ByteArrayLengthSize
 		if n > r {
-			return dst, plain.ErrTooShort(n)
+			return dst, encoding.Error(e, plain.ErrTooShort(n))
 		}
 		if n > plain.MaxByteArrayLength {
-			return dst, plain.ErrTooLarge(n)
+			return dst, encoding.Error(e, plain.ErrTooLarge(n))
 		}
 		length.values = append(length.values, int32(n))
 		totalSize += n
@@ -68,29 +62,19 @@ func (e *LengthByteArrayEncoding) encodeByteArray(dst, src []byte) ([]byte, erro
 }
 
 func (e *LengthByteArrayEncoding) DecodeByteArray(dst, src []byte) ([]byte, error) {
-	return e.decodeByteArray(dst[:0], src)
-}
+	dst = dst[:0]
 
-func (e *LengthByteArrayEncoding) decodeByteArray(dst, src []byte) ([]byte, error) {
 	length := getInt32Buffer()
 	defer putInt32Buffer(length)
 
-	var err error
-	src, err = length.decode(src)
+	src, err := length.decode(src)
 	if err != nil {
-		return dst, err
+		return dst, encoding.Error(e, err)
 	}
 
-	for _, n := range length.values {
-		if int(n) < 0 {
-			return dst, encoding.Errorf(e, "invalid negative value length: %d", n)
-		}
-		if int(n) > len(src) {
-			return dst, encoding.Errorf(e, "value length is larger than the input size: %d > %d", n, len(src))
-		}
-		dst = plain.AppendByteArray(dst, src[:n])
-		src = src[n:]
+	dst, err = decodeLengthByteArray(dst, src, length.values)
+	if err != nil {
+		err = encoding.Error(e, err)
 	}
-
-	return dst, nil
+	return dst, err
 }
diff --git a/encoding/delta/length_byte_array_amd64.go b/encoding/delta/length_byte_array_amd64.go
new file mode 100644
index 0000000..1360153
--- /dev/null
+++ b/encoding/delta/length_byte_array_amd64.go
@@ -0,0 +1,84 @@
+//go:build !purego
+
+package delta
+
+import (
+	"github.com/segmentio/parquet-go/encoding/plain"
+	"golang.org/x/sys/cpu"
+)
+
+//go:noescape
+func validateLengthValuesAVX2(lengths []int32) (totalLength int, ok bool)
+
+func validateLengthValues(lengths []int32, maxLength int) (totalLength int, err error) {
+	if cpu.X86.HasAVX2 {
+		totalLength, ok := validateLengthValuesAVX2(lengths)
+		if ok {
+			return totalLength, nil
+		}
+	}
+
+	for i := range lengths {
+		n := int(lengths[i])
+		if n < 0 {
+			return 0, errInvalidNegativeValueLength(n)
+		}
+		if n > maxLength {
+			return 0, errValueLengthOutOfBounds(n, maxLength)
+		}
+		totalLength += n
+	}
+
+	if totalLength > maxLength {
+		err = errValueLengthOutOfBounds(totalLength, maxLength)
+	}
+	return totalLength, err
+}
+
+//go:noescape
+func decodeLengthByteArrayAVX2(dst, src []byte, lengths []int32) int
+
+func decodeLengthByteArray(dst, src []byte, lengths []int32) ([]byte, error) {
+	totalLength, err := validateLengthValues(lengths, len(src))
+	if err != nil {
+		return dst, err
+	}
+
+	size := plain.ByteArrayLengthSize * len(lengths)
+	size += totalLength
+	src = src[:totalLength]
+	dst = resizeNoMemclr(dst, size+padding)
+
+	i := 0
+	j := 0
+	k := 0
+	n := 0
+
+	// To leverage the SEE optimized implementation of the function we must
+	// create enough padding at the end to prevent the opportunistic reads
+	// and writes from overflowing past the buffer's limits.
+	if cpu.X86.HasAVX2 && len(src) > padding {
+		k = len(lengths)
+
+		for k > 0 && n < padding {
+			k--
+			n += int(lengths[k])
+		}
+
+		if k > 0 && n >= padding {
+			i = decodeLengthByteArrayAVX2(dst, src, lengths[:k])
+			j = len(src) - n
+		} else {
+			k = 0
+		}
+	}
+
+	for _, n := range lengths[k:] {
+		plain.PutByteArrayLength(dst[i:], int(n))
+		i += plain.ByteArrayLengthSize
+		i += copy(dst[i:], src[j:j+int(n)])
+		j += int(n)
+	}
+
+	return dst[:size], nil
+}
diff --git a/encoding/delta/length_byte_array_amd64.s b/encoding/delta/length_byte_array_amd64.s
new file mode 100644
index 0000000..7cc43d0
--- /dev/null
+++ b/encoding/delta/length_byte_array_amd64.s
@@ -0,0 +1,139 @@
+//go:build !purego
+
+#include "funcdata.h"
+#include "textflag.h"
+
+// func validateLengthValuesAVX2(lengths []int32) (totalLength int, ok bool)
+TEXT ·validateLengthValuesAVX2(SB), NOSPLIT, $0-33
+    MOVQ lengths_base+0(FP), AX
+    MOVQ lengths_len+8(FP), CX
+
+    XORQ BX, BX // totalLength
+    XORQ DX, DX // err
+    XORQ SI, SI
+    XORQ DI, DI
+    XORQ R8, R8
+
+    CMPQ CX, $16
+    JB test
+
+    MOVQ CX, DI
+    SHRQ $4, DI
+    SHLQ $4, DI
+
+    VPXOR X0, X0, X0 // totalLengths
+    VPXOR X1, X1, X1 // negative test
+loopAVX2:
+    VMOVDQU (AX)(SI*4), Y2
+    VMOVDQU 32(AX)(SI*4), Y3
+    VPADDD Y2, Y0, Y0
+    VPADDD Y3, Y0, Y0
+    VPOR Y2, Y1, Y1
+    VPOR Y3, Y1, Y1
+    ADDQ $16, SI
+    CMPQ SI, DI
+    JNE loopAVX2
+
+    // If any of the 32 bit words has its most significant bit set to 1,
+    // then at least one of the values was negative, which must be reported as
+    // an error.
+    VMOVMSKPS Y1, R8
+    CMPQ R8, $0
+    JNE done
+
+    VPSRLDQ $4, Y0, Y1
+    VPSRLDQ $8, Y0, Y2
+    VPSRLDQ $12, Y0, Y3
+
+    VPADDD Y1, Y0, Y0
+    VPADDD Y3, Y2, Y2
+    VPADDD Y2, Y0, Y0
+
+    VPERM2I128 $1, Y0, Y0, Y1
+    VPADDD Y1, Y0, Y0
+    VZEROUPPER
+    MOVQ X0, BX
+    ANDQ $0x7FFFFFFF, BX
+
+    JMP test
+loop:
+    MOVL (AX)(SI*4), DI
+    ADDL DI, BX
+    ORL DI, R8
+    INCQ SI
+test:
+    CMPQ SI, CX
+    JNE loop
+    CMPL R8, $0
+    JL done
+    MOVB $1, DX
+done:
+    MOVQ BX, totalLength+24(FP)
+    MOVB DX, ok+32(FP)
+    RET
+
+// This function is an optimization of the decodeLengthByteArray using AVX2
+// instructions to implement an opportunistic copy strategy which improves
+// throughput compared to using runtime.memmove (via Go's copy).
+//
+// Parquet columns of type BYTE_ARRAY will often hold short strings, rarely
+// exceeding a couple hundred bytes in size. Making a function call to
+// runtime.memmove for each value results in spending most of the CPU time
+// on branching rather than actually copying bytes to the output buffer.
+//
+// This function works by always assuming it can copy 16 bytes of data between
+// the input and outputs, even in the event where a value is shorter than this.
+//
+// The pointers to the current positions for input and output pointers are
+// always adjusted by the right number of bytes so that the next writes
+// overwrite any extra bytes that were written in the previous iteration of the
+// copy loop.
+//
+// The throughput of this function is not as good as runtime.memmove for large
+// buffers, but it ends up being close to an order of magnitude higher for the
+// common case of working with short strings.
+//
+// func decodeLengthByteArrayAVX2(dst, src []byte, lengths []int32) int
+TEXT ·decodeLengthByteArrayAVX2(SB), NOSPLIT, $0-80
+    MOVQ dst_base+0(FP), AX
+    MOVQ src_base+24(FP), BX
+    MOVQ lengths_base+48(FP), DX
+    MOVQ lengths_len+56(FP), DI
+
+    LEAQ (DX)(DI*4), DI
+    LEAQ 4(AX), AX
+    XORQ CX, CX
+    JMP test
+loop:
+    MOVL (DX), CX
+    MOVL CX, -4(AX)
+    // First pass moves 16 bytes, this makes it a very fast path for short
+    // strings.
+    VMOVDQU (BX), X0
+    VMOVDQU X0, (AX)
+    CMPQ CX, $16
+    JA copy
+next:
+    LEAQ 4(AX)(CX*1), AX
+    LEAQ 0(BX)(CX*1), BX
+    LEAQ 4(DX), DX
+test:
+    CMPQ DX, DI
+    JNE loop
+    MOVQ dst_base+0(FP), BX
+    SUBQ BX, AX
+    SUBQ $4, AX
+    MOVQ AX, ret+72(FP)
+    VZEROUPPER
+    RET
+copy:
+    // Values longer than 16 bytes enter this loop and move 32 byte chunks
+    // which helps improve throughput on larger chunks.
+    MOVQ $16, SI
+copyLoop32:
+    VMOVDQU (BX)(SI*1), Y0
+    VMOVDQU Y0, (AX)(SI*1)
+    ADDQ $32, SI
+    CMPQ SI, CX
+    JAE next
+    JMP copyLoop32
diff --git a/encoding/delta/length_byte_array_purego.go b/encoding/delta/length_byte_array_purego.go
new file mode 100644
index 0000000..91d6efa
--- /dev/null
+++ b/encoding/delta/length_byte_array_purego.go
@@ -0,0 +1,22 @@
+//go:build purego || !amd64
+
+package delta
+
+import (
+	"github.com/segmentio/parquet-go/encoding/plain"
+)
+
+func decodeLengthByteArray(dst, src []byte, lengths []int32) ([]byte, error) {
+	for i := range lengths {
+		n := int(lengths[i])
+		if n < 0 {
+			return dst, errInvalidNegativeValueLength(n)
+		}
+		if n > len(src) {
+			return dst, errValueLengthOutOfBounds(n, len(src))
+		}
+		dst = plain.AppendByteArray(dst, src[:n])
+		src = src[n:]
+	}
+	return dst, nil
+}
diff --git a/encoding/plain/plain.go b/encoding/plain/plain.go
index e628e83..43191f1 100644
--- a/encoding/plain/plain.go
+++ b/encoding/plain/plain.go
@@ -224,6 +224,12 @@ func AppendByteArrayString(b []byte, v string) []byte {
 	return b
 }
 
+func AppendByteArrayLength(b []byte, n int) []byte {
+	length := [ByteArrayLengthSize]byte{}
+	PutByteArrayLength(length[:], n)
+	return append(b, length[:]...)
+}
+
 func ByteArrayLength(b []byte) int {
 	return int(binary.LittleEndian.Uint32(b))
 }
diff --git a/encoding/plain/plain_amd64.s b/encoding/plain/plain_amd64.s
index b8a6067..748470c 100644
--- a/encoding/plain/plain_amd64.s
+++ b/encoding/plain/plain_amd64.s
@@ -8,8 +8,8 @@
 //
 // func validateByteArrays([]byte) status
 TEXT ·validateByteArrays(SB), NOSPLIT, $0-32
-    MOVQ arg+0(FP), AX
-    MOVQ arg+8(FP), BX
+    MOVQ arg_base+0(FP), AX
+    MOVQ arg_len+8(FP), BX
 
     CMPQ BX, $0
     JE done
@@ -42,5 +42,5 @@ errTooShort:
     RET
 errTooLarge:
     MOVQ $2, CX
-    MOVQ CX, ret+32(FP)
+    MOVQ CX, ret+24(FP)
     RET
diff --git a/encoding/rle/rle.go b/encoding/rle/rle.go
index dd5007e..9fba3df 100644
--- a/encoding/rle/rle.go
+++ b/encoding/rle/rle.go
@@ -13,6 +13,7 @@ import (
 
 	"github.com/segmentio/parquet-go/encoding"
 	"github.com/segmentio/parquet-go/format"
+	"github.com/segmentio/parquet-go/internal/bitpack"
 	"github.com/segmentio/parquet-go/internal/bytealg"
 	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
@@ -21,10 +22,10 @@ const (
 	// This limit is intended to prevent unbounded memory allocations when
 	// decoding runs.
 	//
-	// We use a generous limit which allows for over a million values per page
+	// We use a generous limit which allows for over 16 million values per page
 	// if there is only one run to encode the repetition or definition levels
 	// (this should be uncommon).
-	maxSupportedValueCount = 1024 * 1024
+	maxSupportedValueCount = 16 * 1024 * 1024
 )
 
 type Encoding struct {
@@ -251,21 +252,11 @@ func decodeBits(dst, src []byte) ([]byte, error) {
 		}
 		i += n
 
-		count, bitpack := uint(u>>1), (u&1) != 0
+		count, bitpacked := uint(u>>1), (u&1) != 0
 		if count > maxSupportedValueCount {
 			return dst, fmt.Errorf("decoded run-length block cannot have more than %d values", maxSupportedValueCount)
 		}
-		if !bitpack {
-			word := byte(0)
-			if i < len(src) {
-				word = src[i]
-				i++
-			}
-
-			for n := uint(0); n < count; n += 8 {
-				dst = append(dst, word)
-			}
-		} else {
+		if bitpacked {
 			n := int(count)
 			j := i + n
 
@@ -275,6 +266,17 @@ func decodeBits(dst, src []byte) ([]byte, error) {
 
 			dst = append(dst, src[i:j]...)
 			i = j
+		} else {
+			word := byte(0)
+			if i < len(src) {
+				word = src[i]
+				i++
+			}
+
+			offset := len(dst)
+			length := bitpack.ByteCount(count)
+			dst = resize(dst, offset+length)
+			bytealg.Broadcast(dst[offset:], word)
 		}
 	}
 	return dst, nil
@@ -285,9 +287,6 @@ func decodeBytes(dst, src []byte, bitWidth uint) ([]byte, error) {
 		return dst, errDecodeInvalidBitWidth("INT8", bitWidth)
 	}
 
-	bitMask := uint64(1<<bitWidth) - 1
-	byteCount := byteCount(8 * bitWidth)
-
 	for i := 0; i < len(src); {
 		u, n := binary.Uvarint(src[i:])
 		if n == 0 {
@@ -298,11 +297,25 @@ func decodeBytes(dst, src []byte, bitWidth uint) ([]byte, error) {
 		}
 		i += n
 
-		count, bitpack := uint(u>>1), (u&1) != 0
+		count, bitpacked := uint(u>>1), (u&1) != 0
 		if count > maxSupportedValueCount {
 			return dst, fmt.Errorf("decoded run-length block cannot have more than %d values", maxSupportedValueCount)
 		}
-		if !bitpack {
+		if bitpacked {
+			count *= 8
+			j := i + bitpack.ByteCount(count*bitWidth)
+
+			if j > len(src) {
+				return dst, fmt.Errorf("decoding bit-packed block of %d values: %w", 8*count, io.ErrUnexpectedEOF)
+			}
+
+			offset := len(dst)
+			length := int(count)
+			dst = resize(dst, offset+length)
+			decodeBytesBitpack(dst[offset:], src[i:j], count, bitWidth)
+
+			i = j
+		} else {
 			if bitWidth != 0 && (i+1) > len(src) {
 				return dst, fmt.Errorf("decoding run-length block of %d values: %w", count, io.ErrUnexpectedEOF)
 			}
@@ -313,35 +326,10 @@ func decodeBytes(dst, src []byte, bitWidth uint) ([]byte, error) {
 				i++
 			}
 
-			for count > 0 {
-				dst = append(dst, word)
-				count--
-			}
-		} else {
-			for n := uint(0); n < count; n++ {
-				j := i + byteCount
-
-				if j > len(src) {
-					return dst, fmt.Errorf("decoding bit-packed block of %d values: %w", 8*count, io.ErrUnexpectedEOF)
-				}
-
-				bits := [8]byte{}
-				copy(bits[:], src[i:j])
-				word := binary.LittleEndian.Uint64(bits[:])
-
-				dst = append(dst,
-					byte((word>>(0*bitWidth))&bitMask),
-					byte((word>>(1*bitWidth))&bitMask),
-					byte((word>>(2*bitWidth))&bitMask),
-					byte((word>>(3*bitWidth))&bitMask),
-					byte((word>>(4*bitWidth))&bitMask),
-					byte((word>>(5*bitWidth))&bitMask),
-					byte((word>>(6*bitWidth))&bitMask),
-					byte((word>>(7*bitWidth))&bitMask),
-				)
-
-				i = j
-			}
+			offset := len(dst)
+			length := int(count)
+			dst = resize(dst, offset+length)
+			bytealg.Broadcast(dst[offset:], word)
 		}
 	}
 
@@ -353,9 +341,7 @@ func decodeInt32(dst, src []byte, bitWidth uint) ([]byte, error) {
 		return dst, errDecodeInvalidBitWidth("INT32", bitWidth)
 	}
 
-	bitMask := uint64(1<<bitWidth) - 1
-	byteCount1 := byteCount(1 * bitWidth)
-	byteCount8 := byteCount(8 * bitWidth)
+	buf := make([]byte, 2*bitpack.Padding)
 
 	for i := 0; i < len(src); {
 		u, n := binary.Uvarint(src[i:])
@@ -367,12 +353,33 @@ func decodeInt32(dst, src []byte, bitWidth uint) ([]byte, error) {
 		}
 		i += n
 
-		count, bitpack := uint(u>>1), (u&1) != 0
+		count, bitpacked := uint(u>>1), (u&1) != 0
 		if count > maxSupportedValueCount {
 			return dst, fmt.Errorf("decoded run-length block cannot have more than %d values", maxSupportedValueCount)
 		}
-		if !bitpack {
-			j := i + byteCount1
+		if bitpacked {
+			offset := len(dst)
+			length := int(count * bitWidth)
+			dst = resize(dst, offset+4*8*int(count))
+
+			// The bitpack.UnpackInt32 function requires the input to be padded
+			// or the function panics. If there is enough room in the input
+			// buffer we can use it, otherwise we have to copy it to a larger
+			// location (which should rarely happen).
+			in := src[i : i+length]
+			if (cap(in) - len(in)) >= bitpack.Padding {
+				in = in[:cap(in)]
+			} else {
+				buf = resize(buf, len(in)+bitpack.Padding)
+				copy(buf, in)
+				in = buf
+			}
+
+			out := unsafecast.BytesToInt32(dst[offset:])
+			bitpack.UnpackInt32(out, in, bitWidth)
+			i += length
+		} else {
+			j := i + bitpack.ByteCount(bitWidth)
 
 			if j > len(src) {
 				return dst, fmt.Errorf("decoding run-length block of %d values: %w", count, io.ErrUnexpectedEOF)
@@ -380,37 +387,8 @@ func decodeInt32(dst, src []byte, bitWidth uint) ([]byte, error) {
 
 			bits := [4]byte{}
 			copy(bits[:], src[i:j])
+			dst = appendRepeat(dst, bits[:], count)
 			i = j
-
-			for count > 0 {
-				dst = append(dst, bits[:]...)
-				count--
-			}
-		} else {
-			for n := uint(0); n < count; n++ {
-				j := i + byteCount8
-
-				if j > len(src) {
-					return dst, fmt.Errorf("decoding bit-packed block of %d values: %w", 8*count, io.ErrUnexpectedEOF)
-				}
-
-				value := uint64(0)
-				bitOffset := uint(0)
-
-				for _, b := range src[i:j] {
-					value |= uint64(b) << bitOffset
-
-					for bitOffset += 8; bitOffset >= bitWidth; {
-						buf := [4]byte{}
-						binary.LittleEndian.PutUint32(buf[:], uint32(value&bitMask))
-						dst = append(dst, buf[:]...)
-						value >>= bitWidth
-						bitOffset -= bitWidth
-					}
-				}
-
-				i = j
-			}
 		}
 	}
 
@@ -429,6 +407,17 @@ func errInvalidBitWidth(op, typ string, bitWidth uint) error {
 	return fmt.Errorf("cannot %s %s with invalid bit-width=%d", op, typ, bitWidth)
 }
 
+func appendRepeat(dst, pattern []byte, count uint) []byte {
+	offset := len(dst)
+	length := int(count) * len(pattern)
+	dst = resize(dst, offset+length)
+	i := offset + copy(dst[offset:], pattern)
+	for i < len(dst) {
+		i += copy(dst[i:], dst[offset:i])
+	}
+	return dst
+}
+
 func appendUvarint(dst []byte, u uint64) []byte {
 	var b [binary.MaxVarintLen64]byte
 	var n = binary.PutUvarint(b[:], u)
@@ -468,7 +457,7 @@ func appendRunLengthInt32(dst []byte, count int, value int32, bitWidth uint) []b
 	dst = resize(dst, n+binary.MaxVarintLen64+4)
 	n += binary.PutUvarint(dst[n:], uint64(count)<<1)
 	binary.LittleEndian.PutUint32(dst[n:], uint32(value))
-	return dst[:n+int((bitWidth+7)/8)]
+	return dst[:n+bitpack.ByteCount(bitWidth)]
 }
 
 func appendBitPackedInt32(dst []byte, words [][8]int32, bitWidth uint) []byte {
@@ -487,10 +476,6 @@ func broadcast8x4(v int32) [8]int32 {
 	return [8]int32{v, v, v, v, v, v, v, v}
 }
 
-func byteCount(numBits uint) int {
-	return int((numBits + 7) / 8)
-}
-
 func isZero(data []byte) bool {
 	return bytealg.Count(data, 0x00) == len(data)
 }
@@ -503,12 +488,6 @@ func resize(buf []byte, size int) []byte {
 	if cap(buf) < size {
 		return grow(buf, size)
 	}
-	if size > len(buf) {
-		clear := buf[len(buf):size]
-		for i := range clear {
-			clear[i] = 0
-		}
-	}
 	return buf[:size]
 }
 
@@ -523,19 +502,7 @@ func grow(buf []byte, size int) []byte {
 }
 
 func encodeInt32BitpackDefault(dst []byte, src [][8]int32, bitWidth uint) int {
-	bits := unsafe.Slice((*uint32)(unsafe.Pointer(&dst[0])), len(dst)/4)
-	bitMask := uint32(1<<bitWidth) - 1
-	bitOffset := uint(0)
-
-	for k := range src {
-		for _, value := range src[k] {
-			i := bitOffset / 32
-			j := bitOffset % 32
-			bits[i+0] |= (uint32(value) & bitMask) << j
-			bits[i+1] |= (uint32(value) >> (32 - j))
-			bitOffset += bitWidth
-		}
-	}
-
-	return int(bitOffset / 8)
+	bits := unsafe.Slice((*int32)(unsafe.Pointer(&src[0])), len(src)*8)
+	bitpack.PackInt32(dst, bits, bitWidth)
+	return bitpack.ByteCount(uint(len(src)*8) * bitWidth)
 }
diff --git a/encoding/rle/rle_amd64.go b/encoding/rle/rle_amd64.go
index 1c242f2..37ed50b 100644
--- a/encoding/rle/rle_amd64.go
+++ b/encoding/rle/rle_amd64.go
@@ -18,6 +18,7 @@ func init() {
 		encodeInt32Bitpack = encodeInt32BitpackAVX2
 	default:
 		encodeInt32IndexEqual8Contiguous = encodeInt32IndexEqual8ContiguousSSE
+		encodeInt32Bitpack = encodeInt32BitpackDefault
 	}
 }
 
@@ -43,3 +44,6 @@ func encodeInt32BitpackAVX2(dst []byte, src [][8]int32, bitWidth uint) int {
 		return encodeInt32BitpackDefault(dst, src, bitWidth)
 	}
 }
+
+//go:noescape
+func decodeBytesBitpack(dst, src []byte, count, bitWidth uint)
diff --git a/encoding/rle/rle_amd64.s b/encoding/rle/rle_amd64.s
index 69a10a0..7e46e77 100644
--- a/encoding/rle/rle_amd64.s
+++ b/encoding/rle/rle_amd64.s
@@ -3,8 +3,8 @@
 #include "textflag.h"
 
 GLOBL bitMasks<>(SB), RODATA|NOPTR, $64
-DATA bitMasks<>+0(SB)/8, $0b0000000100000001000000010000000100000001000000010000000100000001
-DATA bitMasks<>+8(SB)/8, $0b0000001100000011000000110000001100000011000000110000001100000011
+DATA bitMasks<>+0(SB)/8,  $0b0000000100000001000000010000000100000001000000010000000100000001
+DATA bitMasks<>+8(SB)/8,  $0b0000001100000011000000110000001100000011000000110000001100000011
 DATA bitMasks<>+16(SB)/8, $0b0000011100000111000001110000011100000111000001110000011100000111
 DATA bitMasks<>+24(SB)/8, $0b0000111100001111000011110000111100001111000011110000111100001111
 DATA bitMasks<>+32(SB)/8, $0b0001111100011111000111110001111100011111000111110001111100011111
@@ -12,11 +12,33 @@ DATA bitMasks<>+40(SB)/8, $0b001111110011111100111111001111110011111100111111001
 DATA bitMasks<>+48(SB)/8, $0b0111111101111111011111110111111101111111011111110111111101111111
 DATA bitMasks<>+56(SB)/8, $0b1111111111111111111111111111111111111111111111111111111111111111
 
+// func decodeBytesBitpack(dst, src []byte, count, bitWidth uint)
+TEXT ·decodeBytesBitpack(SB), NOSPLIT, $0-64
+    MOVQ dst_base+0(FP), AX
+    MOVQ src_base+24(FP), BX
+    MOVQ count+48(FP), CX
+    MOVQ bitWidth+56(FP), DX
+    LEAQ bitMasks<>(SB), DI
+    MOVQ -8(DI)(DX*8), DI
+    XORQ SI, SI
+    SHRQ $3, CX
+    JMP test
+loop:
+    MOVQ (BX), R8
+    PDEPQ DI, R8, R8
+    MOVQ R8, (AX)(SI*8)
+    ADDQ DX, BX
+    INCQ SI
+test:
+    CMPQ SI, CX
+    JNE loop
+    RET
+
 // func encodeBytesBitpack(dst []byte, src []uint64, bitWidth uint) int
 TEXT ·encodeBytesBitpack(SB), NOSPLIT, $0-64
-    MOVQ dst+0(FP), AX
-    MOVQ src+24(FP), BX
-    MOVQ src+32(FP), CX
+    MOVQ dst_base+0(FP), AX
+    MOVQ src_base+24(FP), BX
+    MOVQ src_len+32(FP), CX
     MOVQ bitWidth+48(FP), DX
     LEAQ bitMasks<>(SB), DI
     MOVQ -8(DI)(DX*8), DI
@@ -38,8 +60,8 @@ done:
 
 // func encodeInt32IndexEqual8ContiguousAVX2(words [][8]int32) int
 TEXT ·encodeInt32IndexEqual8ContiguousAVX2(SB), NOSPLIT, $0-32
-    MOVQ words+0(FP), AX
-    MOVQ words+8(FP), BX
+    MOVQ words_base+0(FP), AX
+    MOVQ words_len+8(FP), BX
     XORQ SI, SI
     SHLQ $5, BX
     JMP test
@@ -62,8 +84,8 @@ done:
 
 // func encodeInt32IndexEqual8ContiguousSSE(words [][8]int32) int
 TEXT ·encodeInt32IndexEqual8ContiguousSSE(SB), NOSPLIT, $0-32
-    MOVQ words+0(FP), AX
-    MOVQ words+8(FP), BX
+    MOVQ words_base+0(FP), AX
+    MOVQ words_len+8(FP), BX
     XORQ SI, SI
     SHLQ $5, BX
     JMP test
@@ -89,9 +111,9 @@ done:
 
 // func encodeInt32Bitpack1to16bitsAVX2(dst []byte, src [][8]int32, bitWidth uint) int
 TEXT ·encodeInt32Bitpack1to16bitsAVX2(SB), NOSPLIT, $0-64
-    MOVQ dst+0(FP), AX
-    MOVQ src+24(FP), BX
-    MOVQ src+32(FP), CX
+    MOVQ dst_base+0(FP), AX
+    MOVQ src_base+24(FP), BX
+    MOVQ src_len+32(FP), CX
     MOVQ bitWidth+48(FP), DX
 
     MOVQ DX, X0
diff --git a/encoding/rle/rle_purego.go b/encoding/rle/rle_purego.go
index 0458686..f7584ea 100644
--- a/encoding/rle/rle_purego.go
+++ b/encoding/rle/rle_purego.go
@@ -4,6 +4,8 @@ package rle
 
 import (
 	"encoding/binary"
+
+	"github.com/segmentio/parquet-go/internal/bitpack"
 )
 
 func encodeBytesBitpack(dst []byte, src []uint64, bitWidth uint) int {
@@ -36,3 +38,31 @@ func encodeInt32IndexEqual8Contiguous(words [][8]int32) (n int) {
 func encodeInt32Bitpack(dst []byte, src [][8]int32, bitWidth uint) int {
 	return encodeInt32BitpackDefault(dst, src, bitWidth)
 }
+
+func decodeBytesBitpack(dst, src []byte, count, bitWidth uint) {
+	dst = dst[:0]
+
+	bitMask := uint64(1<<bitWidth) - 1
+	byteCount := bitpack.ByteCount(8 * bitWidth)
+
+	for i := 0; count > 0; count -= 8 {
+		j := i + byteCount
+
+		bits := [8]byte{}
+		copy(bits[:], src[i:j])
+		word := binary.LittleEndian.Uint64(bits[:])
+
+		dst = append(dst,
+			byte((word>>(0*bitWidth))&bitMask),
+			byte((word>>(1*bitWidth))&bitMask),
+			byte((word>>(2*bitWidth))&bitMask),
+			byte((word>>(3*bitWidth))&bitMask),
+			byte((word>>(4*bitWidth))&bitMask),
+			byte((word>>(5*bitWidth))&bitMask),
+			byte((word>>(6*bitWidth))&bitMask),
+			byte((word>>(7*bitWidth))&bitMask),
+		)
+
+		i = j
+	}
+}
diff --git a/internal/bitpack/bitpack.go b/internal/bitpack/bitpack.go
new file mode 100644
index 0000000..e6a1188
--- /dev/null
+++ b/internal/bitpack/bitpack.go
@@ -0,0 +1,8 @@
+// Package bitpack implements efficient bit packing and unpacking routines for
+// integers of various bit widths.
+package bitpack
+
+// ByteCount returns the number of bytes needed to hold the given bit count.
+func ByteCount(bitCount uint) int {
+	return int((bitCount + 7) / 8)
+}
diff --git a/internal/bitpack/masks_int32_amd64.s b/internal/bitpack/masks_int32_amd64.s
new file mode 100644
index 0000000..6ffe4e2
--- /dev/null
+++ b/internal/bitpack/masks_int32_amd64.s
@@ -0,0 +1,1288 @@
+//go:build !purego
+
+#include "textflag.h"
+
+// -----------------------------------------------------------------------------
+// Shuffle masks used to broadcast bytes of bit-packed valued into vector
+// registers at positions where they can then be shifted into the right
+// locations.
+// -----------------------------------------------------------------------------
+
+// Shuffle masks for unpacking values from bit widths 1 to 16.
+//
+// The masks are grouped in 32 bytes chunks containing 2 masks of 16 bytes, with
+// the following layout:
+//
+// - The first mask is used to shuffle values from the 16 bytes of input into
+//   the lower 16 bytes of output. These values are then shifted RIGHT to be
+//   aligned on the begining of each 32 bit word.
+//
+// - The second mask selects values from the 16 bytes of input into the upper
+//   16 bytes of output. These values are then shifted RIGHT to be aligned on
+//   the beginning of each 32 bit word.
+//
+// The bit width is intended to be used as an index into this array, using this
+// formula to convert from the index to a byte offset:
+//
+//      offset = 32 * (bitWidth - 1)
+//
+GLOBL ·shuffleInt32x1to16bits(SB), RODATA|NOPTR, $512
+
+// 1 bit => 32 bits
+// -----------------
+// 0: [a,b,c,d,e,f,g,h]
+// ...
+DATA ·shuffleInt32x1to16bits+0+0(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+0+4(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+0+8(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+0+12(SB)/4, $0x80808000
+
+DATA ·shuffleInt32x1to16bits+0+16(SB)/4, $0x80808000
+DATA ·shuffleInt32x1to16bits+0+20(SB)/4, $0x80808000
+DATA ·shuffleInt32x1to16bits+0+24(SB)/4, $0x80808000
+DATA ·shuffleInt32x1to16bits+0+28(SB)/4, $0x80808000
+
+// 2 bits => 32 bits
+// -----------------
+// 0: [a,a,b,b,c,c,d,d]
+// 1: [e,e,f,f,g,g,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+32+0(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+32+4(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+32+8(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+32+12(SB)/4, $0x80808000
+
+DATA ·shuffleInt32x1to16bits+32+16(SB)/4, $0x80808001
+DATA ·shuffleInt32x1to16bits+32+20(SB)/4, $0x80808001
+DATA ·shuffleInt32x1to16bits+32+24(SB)/4, $0x80808001
+DATA ·shuffleInt32x1to16bits+32+28(SB)/4, $0x80808001
+
+// 3 bits => 32 bits
+// -----------------
+// 0: [a,a,a,b,b,b,c,c]
+// 1: [c,d,d,d,e,e,e,f]
+// 2: [f,f,g,g,g,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+64+0(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+64+4(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+64+8(SB)/4,  $0x80800100
+DATA ·shuffleInt32x1to16bits+64+12(SB)/4, $0x80808001
+
+DATA ·shuffleInt32x1to16bits+64+16(SB)/4, $0x80808001
+DATA ·shuffleInt32x1to16bits+64+20(SB)/4, $0x80800201
+DATA ·shuffleInt32x1to16bits+64+24(SB)/4, $0x80808002
+DATA ·shuffleInt32x1to16bits+64+28(SB)/4, $0x80808002
+
+// 4 bits => 32 bits
+// -----------------
+// 0: [a,a,a,a,b,b,b,b]
+// 1: [c,c,c,c,d,d,d,d]
+// 2: [e,e,e,e,f,f,f,f]
+// 3: [g,g,g,g,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+96+0(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+96+4(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+96+8(SB)/4,  $0x80808001
+DATA ·shuffleInt32x1to16bits+96+12(SB)/4, $0x80808001
+
+DATA ·shuffleInt32x1to16bits+96+16(SB)/4, $0x80808002
+DATA ·shuffleInt32x1to16bits+96+20(SB)/4, $0x80808002
+DATA ·shuffleInt32x1to16bits+96+24(SB)/4, $0x80808003
+DATA ·shuffleInt32x1to16bits+96+28(SB)/4, $0x80808003
+
+// 5 bits => 32 bits
+// -----------------
+// 0: [a,a,a,a,a,b,b,b]
+// 1: [b,b,c,c,c,c,c,d]
+// 2: [d,d,d,d,e,e,e,e]
+// 3: [e,f,f,f,f,f,g,g]
+// 4: [g,g,g,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+128+0(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+128+4(SB)/4,  $0x80800100
+DATA ·shuffleInt32x1to16bits+128+8(SB)/4,  $0x80808001
+DATA ·shuffleInt32x1to16bits+128+12(SB)/4, $0x80800201
+
+DATA ·shuffleInt32x1to16bits+128+16(SB)/4, $0x80800302
+DATA ·shuffleInt32x1to16bits+128+20(SB)/4, $0x80808003
+DATA ·shuffleInt32x1to16bits+128+24(SB)/4, $0x80800403
+DATA ·shuffleInt32x1to16bits+128+28(SB)/4, $0x80808004
+
+// 6 bits => 32 bits
+// -----------------
+// 0: [a,a,a,a,a,a,b,b]
+// 1: [b,b,b,b,c,c,c,c]
+// 2: [c,c,d,d,d,d,d,d]
+// 3: [e,e,e,e,e,e,f,f]
+// 4: [f,f,f,f,g,g,g,g]
+// 5: [g,g,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+160+0(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+160+4(SB)/4,  $0x80800100
+DATA ·shuffleInt32x1to16bits+160+8(SB)/4,  $0x80800201
+DATA ·shuffleInt32x1to16bits+160+12(SB)/4, $0x80808002
+
+DATA ·shuffleInt32x1to16bits+160+16(SB)/4, $0x80808003
+DATA ·shuffleInt32x1to16bits+160+20(SB)/4, $0x80800403
+DATA ·shuffleInt32x1to16bits+160+24(SB)/4, $0x80800504
+DATA ·shuffleInt32x1to16bits+160+28(SB)/4, $0x80808005
+
+// 7 bits => 32 bits
+// -----------------
+// 0: [a,a,a,a,a,a,a,b]
+// 1: [b,b,b,b,b,b,c,c]
+// 2: [c,c,c,c,c,d,d,d]
+// 3: [d,d,d,d,e,e,e,e]
+// 4: [e,e,e,f,f,f,f,f]
+// 5: [f,f,g,g,g,g,g,g]
+// 6: [g,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+192+0(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+192+4(SB)/4,  $0x80800100
+DATA ·shuffleInt32x1to16bits+192+8(SB)/4,  $0x80800201
+DATA ·shuffleInt32x1to16bits+192+12(SB)/4, $0x80800302
+
+DATA ·shuffleInt32x1to16bits+192+16(SB)/4, $0x80800403
+DATA ·shuffleInt32x1to16bits+192+20(SB)/4, $0x80800504
+DATA ·shuffleInt32x1to16bits+192+24(SB)/4, $0x80800605
+DATA ·shuffleInt32x1to16bits+192+28(SB)/4, $0x80808006
+
+// 8 bits => 32 bits
+// -----------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [b,b,b,b,b,b,b,b]
+// 2: [c,c,c,c,c,c,c,c]
+// 3: [d,d,d,d,d,d,d,d]
+// 4: [e,e,e,e,e,e,e,e]
+// 5: [f,f,f,f,f,f,f,f]
+// 6: [g,g,g,g,g,g,g,g]
+// 7: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+224+0(SB)/4,  $0x80808000
+DATA ·shuffleInt32x1to16bits+224+4(SB)/4,  $0x80808001
+DATA ·shuffleInt32x1to16bits+224+8(SB)/4,  $0x80808002
+DATA ·shuffleInt32x1to16bits+224+12(SB)/4, $0x80808003
+
+DATA ·shuffleInt32x1to16bits+224+16(SB)/4, $0x80808004
+DATA ·shuffleInt32x1to16bits+224+20(SB)/4, $0x80808005
+DATA ·shuffleInt32x1to16bits+224+24(SB)/4, $0x80808006
+DATA ·shuffleInt32x1to16bits+224+28(SB)/4, $0x80808007
+
+// 9 bits => 32 bits
+// -----------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,b,b,b,b,b,b,b]
+// 2: [b,b,c,c,c,c,c,c]
+// 3: [c,c,c,d,d,d,d,d]
+// 4: [d,d,d,d,e,e,e,e]
+// 5: [e,e,e,e,e,f,f,f]
+// 6: [f,f,f,f,f,f,g,g]
+// 7: [g,g,g,g,g,g,g,h]
+// 8: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+256+0(SB)/4,  $0x80800100
+DATA ·shuffleInt32x1to16bits+256+4(SB)/4,  $0x80800201
+DATA ·shuffleInt32x1to16bits+256+8(SB)/4,  $0x80800302
+DATA ·shuffleInt32x1to16bits+256+12(SB)/4, $0x80800403
+
+DATA ·shuffleInt32x1to16bits+256+16(SB)/4, $0x80800504
+DATA ·shuffleInt32x1to16bits+256+20(SB)/4, $0x80800605
+DATA ·shuffleInt32x1to16bits+256+24(SB)/4, $0x80800706
+DATA ·shuffleInt32x1to16bits+256+28(SB)/4, $0x80800807
+
+// 10 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,b,b,b,b,b,b]
+// 2: [b,b,b,b,c,c,c,c]
+// 3: [c,c,c,c,c,c,d,d]
+// 4: [d,d,d,d,d,d,d,d]
+// 5: [e,e,e,e,e,e,e,e]
+// 6: [e,e,f,f,f,f,f,f]
+// 7: [f,f,f,f,g,g,g,g]
+// 8: [g,g,g,g,g,g,h,h]
+// 9: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+288+0(SB)/4,  $0x80800100
+DATA ·shuffleInt32x1to16bits+288+4(SB)/4,  $0x80800201
+DATA ·shuffleInt32x1to16bits+288+8(SB)/4,  $0x80800302
+DATA ·shuffleInt32x1to16bits+288+12(SB)/4, $0x80800403
+
+DATA ·shuffleInt32x1to16bits+288+16(SB)/4, $0x80800605
+DATA ·shuffleInt32x1to16bits+288+20(SB)/4, $0x80800706
+DATA ·shuffleInt32x1to16bits+288+24(SB)/4, $0x80800807
+DATA ·shuffleInt32x1to16bits+288+28(SB)/4, $0x80800908
+
+// 11 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,b,b,b,b,b]
+// 2: [b,b,b,b,b,b,c,c]
+// 3: [c,c,c,c,c,c,c,c]
+// 4: [c,d,d,d,d,d,d,d]
+// 5: [d,d,d,d,e,e,e,e]
+// 6: [e,e,e,e,e,e,e,f]
+// 7: [f,f,f,f,f,f,f,f]
+// 8: [f,f,g,g,g,g,g,g]
+// 9: [g,g,g,g,g,h,h,h]
+// A: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+320+0(SB)/4,  $0x80800100
+DATA ·shuffleInt32x1to16bits+320+4(SB)/4,  $0x80800201
+DATA ·shuffleInt32x1to16bits+320+8(SB)/4,  $0x80040302
+DATA ·shuffleInt32x1to16bits+320+12(SB)/4, $0x80800504
+
+DATA ·shuffleInt32x1to16bits+320+16(SB)/4, $0x80800605
+DATA ·shuffleInt32x1to16bits+320+20(SB)/4, $0x80080706
+DATA ·shuffleInt32x1to16bits+320+24(SB)/4, $0x80800908
+DATA ·shuffleInt32x1to16bits+320+28(SB)/4, $0x80800A09
+
+// 12 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,b,b,b,b]
+// 2: [b,b,b,b,b,b,b,b]
+// 3: [c,c,c,c,c,c,c,c]
+// 4: [c,c,c,c,d,d,d,d]
+// 5: [d,d,d,d,d,d,d,d]
+// 6: [e,e,e,e,e,e,e,e]
+// 7: [e,e,e,e,f,f,f,f]
+// 8: [f,f,f,f,f,f,f,f]
+// 9: [g,g,g,g,g,g,g,g]
+// A: [g,g,g,g,h,h,h,h]
+// B: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+352+0(SB)/4,  $0x80800100
+DATA ·shuffleInt32x1to16bits+352+4(SB)/4,  $0x80800201
+DATA ·shuffleInt32x1to16bits+352+8(SB)/4,  $0x80080403
+DATA ·shuffleInt32x1to16bits+352+12(SB)/4, $0x80800504
+
+DATA ·shuffleInt32x1to16bits+352+16(SB)/4, $0x80800706
+DATA ·shuffleInt32x1to16bits+352+20(SB)/4, $0x80800807
+DATA ·shuffleInt32x1to16bits+352+24(SB)/4, $0x80800A09
+DATA ·shuffleInt32x1to16bits+352+28(SB)/4, $0x80800B0A
+
+// 13 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,b,b,b]
+// 2: [b,b,b,b,b,b,b,b]
+// 3: [b,b,c,c,c,c,c,c]
+// 4: [c,c,c,c,c,c,c,d]
+// 5: [d,d,d,d,d,d,d,d]
+// 6: [d,d,d,d,e,e,e,e]
+// 7: [e,e,e,e,e,e,e,e]
+// 8: [e,f,f,f,f,f,f,f]
+// 9: [f,f,f,f,f,f,g,g]
+// A: [g,g,g,g,g,g,g,g]
+// B: [g,g,g,h,h,h,h,h]
+// C: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+384+0(SB)/4,  $0x80800100
+DATA ·shuffleInt32x1to16bits+384+4(SB)/4,  $0x80030201
+DATA ·shuffleInt32x1to16bits+384+8(SB)/4,  $0x80800403
+DATA ·shuffleInt32x1to16bits+384+12(SB)/4, $0x80060504
+
+DATA ·shuffleInt32x1to16bits+384+16(SB)/4, $0x80080706
+DATA ·shuffleInt32x1to16bits+384+20(SB)/4, $0x80800908
+DATA ·shuffleInt32x1to16bits+384+24(SB)/4, $0x800B0A09
+DATA ·shuffleInt32x1to16bits+384+28(SB)/4, $0x80800C0B
+
+// 14 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,b,b]
+// 2: [b,b,b,b,b,b,b,b]
+// 3: [b,b,b,b,c,c,c,c]
+// 4: [c,c,c,c,c,c,c,c]
+// 5: [c,c,d,d,d,d,d,d]
+// 6: [d,d,d,d,d,d,d,d]
+// 7: [e,e,e,e,e,e,e,e]
+// 8: [e,e,e,e,e,e,f,f]
+// 9: [f,f,f,f,f,f,f,f]
+// A: [f,f,f,f,g,g,g,g]
+// B: [g,g,g,g,g,g,g,g]
+// C: [g,g,h,h,h,h,h,h]
+// D: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+416+0(SB)/4,  $0x80800100
+DATA ·shuffleInt32x1to16bits+416+4(SB)/4,  $0x80030201
+DATA ·shuffleInt32x1to16bits+416+8(SB)/4,  $0x80050403
+DATA ·shuffleInt32x1to16bits+416+12(SB)/4, $0x80800605
+
+DATA ·shuffleInt32x1to16bits+416+16(SB)/4, $0x80080807
+DATA ·shuffleInt32x1to16bits+416+20(SB)/4, $0x800A0908
+DATA ·shuffleInt32x1to16bits+416+24(SB)/4, $0x800C0B0A
+DATA ·shuffleInt32x1to16bits+416+28(SB)/4, $0x80800D0C
+
+// 15 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,b]
+// 2: [b,b,b,b,b,b,b,b]
+// 3: [b,b,b,b,b,b,c,c]
+// 4: [c,c,c,c,c,c,c,c]
+// 5: [c,c,c,c,c,d,d,d]
+// 6: [d,d,d,d,d,d,d,d]
+// 7: [d,d,d,d,e,e,e,e]
+// 8: [e,e,e,e,e,e,e,e]
+// 9: [e,e,e,f,f,f,f,f]
+// A: [f,f,f,f,f,f,f,f]
+// B: [f,f,g,g,g,g,g,g]
+// C: [g,g,g,g,g,g,g,g]
+// D: [g,h,h,h,h,h,h,h]
+// E: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+448+0(SB)/4,  $0x80800100
+DATA ·shuffleInt32x1to16bits+448+4(SB)/4,  $0x80030201
+DATA ·shuffleInt32x1to16bits+448+8(SB)/4,  $0x80050403
+DATA ·shuffleInt32x1to16bits+448+12(SB)/4, $0x80070605
+
+DATA ·shuffleInt32x1to16bits+448+16(SB)/4, $0x80090807
+DATA ·shuffleInt32x1to16bits+448+20(SB)/4, $0x800B0A09
+DATA ·shuffleInt32x1to16bits+448+24(SB)/4, $0x800D0C0B
+DATA ·shuffleInt32x1to16bits+448+28(SB)/4, $0x80800E0D
+
+// 16 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [b,b,b,b,b,b,b,b]
+// 3: [b,b,b,b,b,b,c,b]
+// 4: [c,c,c,c,c,c,c,c]
+// 5: [c,c,c,c,c,c,c,c]
+// 6: [d,d,d,d,d,d,d,d]
+// 7: [d,d,d,d,d,d,d,d]
+// 8: [e,e,e,e,e,e,e,e]
+// 9: [e,e,e,e,e,e,e,e]
+// A: [f,f,f,f,f,f,f,f]
+// B: [f,f,f,f,f,f,f,f]
+// C: [g,g,g,g,g,g,g,g]
+// D: [g,g,g,g,g,g,g,g]
+// E: [h,h,h,h,h,h,h,h]
+// F: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x1to16bits+480+0(SB)/4,  $0x80800100
+DATA ·shuffleInt32x1to16bits+480+4(SB)/4,  $0x80800302
+DATA ·shuffleInt32x1to16bits+480+8(SB)/4,  $0x80800504
+DATA ·shuffleInt32x1to16bits+480+12(SB)/4, $0x80800706
+
+DATA ·shuffleInt32x1to16bits+480+16(SB)/4, $0x80800908
+DATA ·shuffleInt32x1to16bits+480+20(SB)/4, $0x80800B0A
+DATA ·shuffleInt32x1to16bits+480+24(SB)/4, $0x80800D0C
+DATA ·shuffleInt32x1to16bits+480+28(SB)/4, $0x80800F0E
+
+// Shuffle masks for unpacking values from bit widths 17 to 26.
+//
+// The masks are grouped in 48 bytes chunks containing 3 masks of 16 bytes, with
+// the following layout:
+//
+// - The first mask is used to shuffle values from the first 16 bytes of input
+//   into the lower 16 bytes of output. These values are then shifted RIGHT to
+//   be aligned on the begining of each 32 bit word.
+//
+// - The second mask selects values from the first 16 bytes of input into the
+//   upper 16 bytes of output. These values are then shifted RIGHT to be aligned
+//   on the beginning of each 32 bit word.
+//
+// - The third mask selects values from the second 16 bytes of input into the
+//   upper 16 bytes of output. These values are then shifted RIGHT to be aligned
+//   on the beginning of each 32 bit word.
+//
+// The bit width is intended to be used as an index into this array, using this
+// formula to convert from the index to a byte offset:
+//
+//      offset = 48 * (bitWidth - 17)
+//
+GLOBL ·shuffleInt32x17to26bits(SB), RODATA|NOPTR, $480
+
+// 17 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,b,b,b,b,b,b,b]
+// 3: [b,b,b,b,b,b,b,b]
+// 4: [b,b,c,c,c,c,c,c]
+// 5: [c,c,c,c,c,c,c,c]
+// 6: [c,c,c,d,d,d,d,d]
+// 7: [d,d,d,d,d,d,d,d]
+// 8: [d,d,d,d,e,e,e,e]
+// 9: [e,e,e,e,e,e,e,e]
+// A: [e,e,e,e,e,f,f,f]
+// B: [f,f,f,f,f,f,f,f]
+// C: [f,f,f,f,f,f,g,g]
+// D: [g,g,g,g,g,g,g,g]
+// E: [g,g,g,g,g,g,g,h]
+// F: [h,h,h,h,h,h,h,h]
+// ---
+// 0: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x17to26bits+0+0(SB)/4,  $0x80020100
+DATA ·shuffleInt32x17to26bits+0+4(SB)/4,  $0x80040302
+DATA ·shuffleInt32x17to26bits+0+8(SB)/4,  $0x80060504
+DATA ·shuffleInt32x17to26bits+0+12(SB)/4, $0x80080706
+
+DATA ·shuffleInt32x17to26bits+0+16(SB)/4, $0x800A0908
+DATA ·shuffleInt32x17to26bits+0+20(SB)/4, $0x800C0B0A
+DATA ·shuffleInt32x17to26bits+0+24(SB)/4, $0x800E0D0C
+DATA ·shuffleInt32x17to26bits+0+28(SB)/4, $0x80800F0E
+
+DATA ·shuffleInt32x17to26bits+0+32(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+0+36(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+0+40(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+0+44(SB)/4, $0x80008080
+
+// 18 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,b,b,b,b,b,b]
+// 3: [b,b,b,b,b,b,b,b]
+// 4: [b,b,b,b,c,c,c,c]
+// 5: [c,c,c,c,c,c,c,c]
+// 6: [c,c,c,c,c,c,d,d]
+// 7: [d,d,d,d,d,d,d,d]
+// 8: [d,d,d,d,d,d,d,d]
+// 9: [e,e,e,e,e,e,e,e]
+// A: [e,e,e,e,e,e,e,e]
+// B: [e,e,f,f,f,f,f,f]
+// C: [f,f,f,f,f,f,f,f]
+// D: [f,f,f,f,g,g,g,g]
+// E: [g,g,g,g,g,g,g,g]
+// F: [g,g,g,g,g,g,h,h]
+// ---
+// 0: [h,h,h,h,h,h,h,h]
+// 1: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x17to26bits+48+0(SB)/4,  $0x80020100
+DATA ·shuffleInt32x17to26bits+48+4(SB)/4,  $0x80040302
+DATA ·shuffleInt32x17to26bits+48+8(SB)/4,  $0x80060504
+DATA ·shuffleInt32x17to26bits+48+12(SB)/4, $0x80080706
+
+DATA ·shuffleInt32x17to26bits+48+16(SB)/4, $0x800B0A09
+DATA ·shuffleInt32x17to26bits+48+20(SB)/4, $0x800D0C0B
+DATA ·shuffleInt32x17to26bits+48+24(SB)/4, $0x800F0E0D
+DATA ·shuffleInt32x17to26bits+48+28(SB)/4, $0x8080800F
+
+DATA ·shuffleInt32x17to26bits+48+32(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+48+36(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+48+40(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+48+44(SB)/4, $0x80010080
+
+// 19 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,b,b,b,b,b]
+// 3: [b,b,b,b,b,b,b,b]
+// 4: [b,b,b,b,b,b,c,c]
+// 5: [c,c,c,c,c,c,c,c]
+// 6: [c,c,c,c,c,c,c,c]
+// 7: [c,d,d,d,d,d,d,d]
+// 8: [d,d,d,d,d,d,d,d]
+// 9: [d,d,d,d,e,e,e,e]
+// A: [e,e,e,e,e,e,e,e]
+// B: [e,e,e,e,e,e,e,f]
+// C: [f,f,f,f,f,f,f,f]
+// D: [f,f,f,f,f,f,f,f]
+// E: [f,f,g,g,g,g,g,g]
+// F: [g,g,g,g,g,g,g,g]
+// ---
+// 0: [g,g,g,g,g,h,h,h]
+// 1: [h,h,h,h,h,h,h,h]
+// 2: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x17to26bits+96+0(SB)/4,  $0x80020100
+DATA ·shuffleInt32x17to26bits+96+4(SB)/4,  $0x80040302
+DATA ·shuffleInt32x17to26bits+96+8(SB)/4,  $0x07060504
+DATA ·shuffleInt32x17to26bits+96+12(SB)/4, $0x80090807
+
+DATA ·shuffleInt32x17to26bits+96+16(SB)/4, $0x800B0A09
+DATA ·shuffleInt32x17to26bits+96+20(SB)/4, $0x0E0D0C0B
+DATA ·shuffleInt32x17to26bits+96+24(SB)/4, $0x80800F0E
+DATA ·shuffleInt32x17to26bits+96+28(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x17to26bits+96+32(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+96+36(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+96+40(SB)/4, $0x80008080
+DATA ·shuffleInt32x17to26bits+96+44(SB)/4, $0x80020100
+
+// 20 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,a,b,b,b,b]
+// 3: [b,b,b,b,b,b,b,b]
+// 4: [b,b,b,b,b,b,b,b]
+// 5: [c,c,c,c,c,c,c,c]
+// 6: [c,c,c,c,c,c,c,c]
+// 7: [c,c,c,c,d,d,d,d]
+// 8: [d,d,d,d,d,d,d,d]
+// 9: [d,d,d,d,d,d,d,d]
+// A: [e,e,e,e,e,e,e,e]
+// B: [e,e,e,e,e,e,e,e]
+// C: [e,e,e,e,f,f,f,f]
+// D: [f,f,f,f,f,f,f,f]
+// E: [f,f,f,f,f,f,f,f]
+// F: [g,g,g,g,g,g,g,g]
+// ---
+// 0: [g,g,g,g,g,g,g,g]
+// 1: [g,g,g,g,h,h,h,h]
+// 2: [h,h,h,h,h,h,h,h]
+// 3: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x17to26bits+144+0(SB)/4,  $0x80020100
+DATA ·shuffleInt32x17to26bits+144+4(SB)/4,  $0x80040302
+DATA ·shuffleInt32x17to26bits+144+8(SB)/4,  $0x80070605
+DATA ·shuffleInt32x17to26bits+144+12(SB)/4, $0x80090807
+
+DATA ·shuffleInt32x17to26bits+144+16(SB)/4, $0x800C0B0A
+DATA ·shuffleInt32x17to26bits+144+20(SB)/4, $0x800E0D0C
+DATA ·shuffleInt32x17to26bits+144+24(SB)/4, $0x8080800F
+DATA ·shuffleInt32x17to26bits+144+28(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x17to26bits+144+32(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+144+36(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+144+40(SB)/4, $0x80010080
+DATA ·shuffleInt32x17to26bits+144+44(SB)/4, $0x80030201
+
+// 21 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,a,a,b,b,b]
+// 3: [b,b,b,b,b,b,b,b]
+// 4: [b,b,b,b,b,b,b,b]
+// 5: [b,b,c,c,c,c,c,c]
+// 6: [c,c,c,c,c,c,c,c]
+// 7: [c,c,c,c,c,c,c,d]
+// 8: [d,d,d,d,d,d,d,d]
+// 9: [d,d,d,d,d,d,d,d]
+// A: [d,d,d,d,e,e,e,e]
+// B: [e,e,e,e,e,e,e,e]
+// C: [e,e,e,e,e,e,e,e]
+// D: [e,f,f,f,f,f,f,f]
+// E: [f,f,f,f,f,f,f,f]
+// F: [f,f,f,f,f,f,g,g]
+// ---
+// 0: [g,g,g,g,g,g,g,g]
+// 1: [g,g,g,g,g,g,g,g]
+// 2: [g,g,g,h,h,h,h,h]
+// 3: [h,h,h,h,h,h,h,h]
+// 4: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x17to26bits+192+0(SB)/4,  $0x80020100
+DATA ·shuffleInt32x17to26bits+192+4(SB)/4,  $0x05040302
+DATA ·shuffleInt32x17to26bits+192+8(SB)/4,  $0x80070605
+DATA ·shuffleInt32x17to26bits+192+12(SB)/4, $0x0A090807
+
+DATA ·shuffleInt32x17to26bits+192+16(SB)/4, $0x0D0C0B0A
+DATA ·shuffleInt32x17to26bits+192+20(SB)/4, $0x800F0E0D
+DATA ·shuffleInt32x17to26bits+192+24(SB)/4, $0x8080800F
+DATA ·shuffleInt32x17to26bits+192+28(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x17to26bits+192+32(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+192+36(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+192+40(SB)/4, $0x02010080
+DATA ·shuffleInt32x17to26bits+192+44(SB)/4, $0x80040302
+
+// 22 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,a,a,a,b,b]
+// 3: [b,b,b,b,b,b,b,b]
+// 4: [b,b,b,b,b,b,b,b]
+// 5: [b,b,b,b,c,c,c,c]
+// 6: [c,c,c,c,c,c,c,c]
+// 7: [c,c,c,c,c,c,c,c]
+// 8: [c,c,d,d,d,d,d,d]
+// 9: [d,d,d,d,d,d,d,d]
+// A: [d,d,d,d,d,d,d,d]
+// B: [e,e,e,e,e,e,e,e]
+// C: [e,e,e,e,e,e,e,e]
+// D: [e,e,e,e,e,e,f,f]
+// E: [f,f,f,f,f,f,f,f]
+// F: [f,f,f,f,f,f,f,f]
+// ---
+// 0: [f,f,f,f,g,g,g,g]
+// 1: [g,g,g,g,g,g,g,g]
+// 2: [g,g,g,g,g,g,g,g]
+// 3: [g,g,h,h,h,h,h,h]
+// 4: [h,h,h,h,h,h,h,h]
+// 5: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x17to26bits+240+0(SB)/4,  $0x80020100
+DATA ·shuffleInt32x17to26bits+240+4(SB)/4,  $0x05040302
+DATA ·shuffleInt32x17to26bits+240+8(SB)/4,  $0x08070605
+DATA ·shuffleInt32x17to26bits+240+12(SB)/4, $0x800A0908
+
+DATA ·shuffleInt32x17to26bits+240+16(SB)/4, $0x800D0C0B
+DATA ·shuffleInt32x17to26bits+240+20(SB)/4, $0x800F0E0D
+DATA ·shuffleInt32x17to26bits+240+24(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+240+28(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x17to26bits+240+32(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+240+36(SB)/4, $0x00808080
+DATA ·shuffleInt32x17to26bits+240+40(SB)/4, $0x03020100
+DATA ·shuffleInt32x17to26bits+240+44(SB)/4, $0x80050403
+
+// 23 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,a,a,a,a,b]
+// 3: [b,b,b,b,b,b,b,b]
+// 4: [b,b,b,b,b,b,b,b]
+// 5: [b,b,b,b,b,b,c,c]
+// 6: [c,c,c,c,c,c,c,c]
+// 7: [c,c,c,c,c,c,c,c]
+// 8: [c,c,c,c,c,d,d,d]
+// 9: [d,d,d,d,d,d,d,d]
+// A: [d,d,d,d,d,d,d,d]
+// B: [d,d,d,d,e,e,e,e]
+// C: [e,e,e,e,e,e,e,e]
+// D: [e,e,e,e,e,e,e,e]
+// E: [e,e,e,f,f,f,f,f]
+// F: [f,f,f,f,f,f,f,f]
+// ---
+// 0: [f,f,f,f,f,f,f,f]
+// 1: [f,f,g,g,g,g,g,g]
+// 2: [g,g,g,g,g,g,g,g]
+// 3: [g,g,g,g,g,g,g,g]
+// 4: [g,h,h,h,h,h,h,h]
+// 5: [h,h,h,h,h,h,h,h]
+// 6: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x17to26bits+288+0(SB)/4,  $0x80020100
+DATA ·shuffleInt32x17to26bits+288+4(SB)/4,  $0x05040302
+DATA ·shuffleInt32x17to26bits+288+8(SB)/4,  $0x08070605
+DATA ·shuffleInt32x17to26bits+288+12(SB)/4, $0x0B0A0908
+
+DATA ·shuffleInt32x17to26bits+288+16(SB)/4, $0x0E0D0C0B
+DATA ·shuffleInt32x17to26bits+288+20(SB)/4, $0x80800F0E
+DATA ·shuffleInt32x17to26bits+288+24(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+288+28(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x17to26bits+288+32(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+288+36(SB)/4, $0x01008080
+DATA ·shuffleInt32x17to26bits+288+40(SB)/4, $0x04030201
+DATA ·shuffleInt32x17to26bits+288+44(SB)/4, $0x80060504
+
+// 24 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,a,a,a,a,a]
+// 3: [b,b,b,b,b,b,b,b]
+// 4: [b,b,b,b,b,b,b,b]
+// 5: [b,b,b,b,b,b,b,b]
+// 6: [c,c,c,c,c,c,c,c]
+// 7: [c,c,c,c,c,c,c,c]
+// 8: [c,c,c,c,c,c,c,c]
+// 9: [d,d,d,d,d,d,d,d]
+// A: [d,d,d,d,d,d,d,d]
+// B: [d,d,d,d,d,d,d,d]
+// C: [e,e,e,e,e,e,e,e]
+// D: [e,e,e,e,e,e,e,e]
+// E: [e,e,e,e,e,e,e,e]
+// F: [f,f,f,f,f,f,f,f]
+// ---
+// 0: [f,f,f,f,f,f,f,f]
+// 1: [f,f,f,f,f,f,f,f]
+// 2: [g,g,g,g,g,g,g,g]
+// 3: [g,g,g,g,g,g,g,g]
+// 4: [g,g,g,g,g,g,g,g]
+// 5: [h,h,h,h,h,h,h,h]
+// 6: [h,h,h,h,h,h,h,h]
+// 7: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x17to26bits+336+0(SB)/4,  $0x80020100
+DATA ·shuffleInt32x17to26bits+336+4(SB)/4,  $0x80050403
+DATA ·shuffleInt32x17to26bits+336+8(SB)/4,  $0x80080706
+DATA ·shuffleInt32x17to26bits+336+12(SB)/4, $0x800B0A09
+
+DATA ·shuffleInt32x17to26bits+336+16(SB)/4, $0x800E0D0C
+DATA ·shuffleInt32x17to26bits+336+20(SB)/4, $0x8080800F
+DATA ·shuffleInt32x17to26bits+336+24(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+336+28(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x17to26bits+336+32(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+336+36(SB)/4, $0x80010080
+DATA ·shuffleInt32x17to26bits+336+40(SB)/4, $0x80040302
+DATA ·shuffleInt32x17to26bits+336+44(SB)/4, $0x80070605
+
+// 25 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,a,a,a,a,a]
+// 3: [a,b,b,b,b,b,b,b]
+// 4: [b,b,b,b,b,b,b,b]
+// 5: [b,b,b,b,b,b,b,b]
+// 6: [b,b,c,c,c,c,c,c]
+// 7: [c,c,c,c,c,c,c,c]
+// 8: [c,c,c,c,c,c,c,c]
+// 9: [c,c,c,d,d,d,d,d]
+// A: [d,d,d,d,d,d,d,d]
+// B: [d,d,d,d,d,d,d,d]
+// C: [d,d,d,d,e,e,e,e]
+// D: [e,e,e,e,e,e,e,e]
+// E: [e,e,e,e,e,e,e,e]
+// F: [e,e,e,e,e,f,f,f]
+// ---
+// 0: [f,f,f,f,f,f,f,f]
+// 1: [f,f,f,f,f,f,f,f]
+// 2: [f,f,f,f,f,f,g,g]
+// 3: [g,g,g,g,g,g,g,g]
+// 4: [g,g,g,g,g,g,g,g]
+// 5: [g,g,g,g,g,g,g,h]
+// 6: [h,h,h,h,h,h,h,h]
+// 7: [h,h,h,h,h,h,h,h]
+// 8: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x17to26bits+384+0(SB)/4,  $0x03020100
+DATA ·shuffleInt32x17to26bits+384+4(SB)/4,  $0x06050403
+DATA ·shuffleInt32x17to26bits+384+8(SB)/4,  $0x09080706
+DATA ·shuffleInt32x17to26bits+384+12(SB)/4, $0x0C0B0A09
+
+DATA ·shuffleInt32x17to26bits+384+16(SB)/4, $0x0F0E0D0C
+DATA ·shuffleInt32x17to26bits+384+20(SB)/4, $0x8080800F
+DATA ·shuffleInt32x17to26bits+384+24(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+384+28(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x17to26bits+384+32(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+384+36(SB)/4, $0x02010080
+DATA ·shuffleInt32x17to26bits+384+40(SB)/4, $0x05040302
+DATA ·shuffleInt32x17to26bits+384+44(SB)/4, $0x08070605
+
+// 26 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,a,a,a,a,a]
+// 3: [a,a,b,b,b,b,b,b]
+// 4: [b,b,b,b,b,b,b,b]
+// 5: [b,b,b,b,b,b,b,b]
+// 6: [b,b,b,b,c,c,c,c]
+// 7: [c,c,c,c,c,c,c,c]
+// 8: [c,c,c,c,c,c,c,c]
+// 9: [c,c,c,c,c,c,d,d]
+// A: [d,d,d,d,d,d,d,d]
+// B: [d,d,d,d,d,d,d,d]
+// C: [d,d,d,d,d,d,d,d]
+// D: [e,e,e,e,e,e,e,e]
+// E: [e,e,e,e,e,e,e,e]
+// F: [e,e,e,e,e,e,e,e]
+// ---
+// 0: [e,e,f,f,f,f,f,f]
+// 1: [f,f,f,f,f,f,f,f]
+// 2: [f,f,f,f,f,f,f,f]
+// 3: [f,f,f,f,g,g,g,g]
+// 4: [g,g,g,g,g,g,g,g]
+// 5: [g,g,g,g,g,g,g,g]
+// 6: [g,g,g,g,g,g,h,h]
+// 7: [h,h,h,h,h,h,h,h]
+// 8: [h,h,h,h,h,h,h,h]
+// 9: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x17to26bits+432+0(SB)/4,  $0x03020100
+DATA ·shuffleInt32x17to26bits+432+4(SB)/4,  $0x06050403
+DATA ·shuffleInt32x17to26bits+432+8(SB)/4,  $0x09080706
+DATA ·shuffleInt32x17to26bits+432+12(SB)/4, $0x0C0B0A09
+
+DATA ·shuffleInt32x17to26bits+432+16(SB)/4, $0x800F0E0D
+DATA ·shuffleInt32x17to26bits+432+20(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+432+24(SB)/4, $0x80808080
+DATA ·shuffleInt32x17to26bits+432+28(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x17to26bits+432+32(SB)/4, $0x00808080
+DATA ·shuffleInt32x17to26bits+432+36(SB)/4, $0x03020100
+DATA ·shuffleInt32x17to26bits+432+40(SB)/4, $0x06050403
+DATA ·shuffleInt32x17to26bits+432+44(SB)/4, $0x09080706
+
+// Shuffle masks for unpacking values from bit widths 27 to 31.
+//
+// The masks are grouped in 80 bytes chunks containing 5 masks of 16 bytes, with
+// the following layout:
+//
+// - The first mask is used to shuffle values from the first 16 bytes of input
+//   into the lower 16 bytes of output. These values are then shifted RIGHT to
+//   be aligned on the begining of each 32 bit word.
+//
+// - The second mask is used to shuffle upper bits of bit-packed values of the
+//   first 16 bytes of input that spanned across 5 bytes. These extra bits cannot
+//   be selected by the first mask (which can select at most 4 bytes per word).
+//   The extra bits are then shifted LEFT to be positioned at the end of the
+//   words, after the bits extracted by the first mask.
+//
+// - The third mask selects values from the first 16 bytes of input into the
+//   upper 16 bytes of output. These values are then shifted RIGHT to be aligned
+//   on the beginning of each 32 bit word.
+//
+// - The fourth mask selects values from the second 16 bytes of input into the
+//   upper 16 bytes of output. These values are then shifted RIGHT to be aligned
+//   on the beginning of each 32 bit word.
+//
+// - The fifth mask is used to shuffle upper bits of bit-packed values values of
+//   second 16 bytes of input that spanned across 5 bytes. These values are then
+//   shifted LEFT to be aligned on the beginning of each 32 bit word.
+//
+// The bit width is intended to be used as an index into this array, using this
+// formula to convert from the index to a byte offset:
+//
+//      offset = 80 * (bitWidth - 27)
+//
+GLOBL ·shuffleInt32x27to31bits(SB), RODATA|NOPTR, $400
+
+// 27 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,a,a,a,a,a]
+// 3: [a,a,a,b,b,b,b,b]
+// 4: [b,b,b,b,b,b,b,b]
+// 5: [b,b,b,b,b,b,b,b]
+// 6: [b,b,b,b,b,b,c,c]
+// 7: [c,c,c,c,c,c,c,c]
+// 8: [c,c,c,c,c,c,c,c]
+// 9: [c,c,c,c,c,c,c,c]
+// A: [c,d,d,d,d,d,d,d]
+// B: [d,d,d,d,d,d,d,d]
+// C: [d,d,d,d,d,d,d,d]
+// D: [d,d,d,d,e,e,e,e]
+// E: [e,e,e,e,e,e,e,e]
+// F: [e,e,e,e,e,e,e,e]
+// ---
+// 0: [e,e,e,e,e,e,e,f]
+// 1: [f,f,f,f,f,f,f,f]
+// 2: [f,f,f,f,f,f,f,f]
+// 3: [f,f,f,f,f,f,f,f]
+// 4: [f,f,g,g,g,g,g,g]
+// 5: [g,g,g,g,g,g,g,g]
+// 6: [g,g,g,g,g,g,g,g]
+// 7: [g,g,g,g,g,h,h,h]
+// 8: [h,h,h,h,h,h,h,h]
+// 9: [h,h,h,h,h,h,h,h]
+// A: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x27to31bits+0+0(SB)/4,  $0x03020100
+DATA ·shuffleInt32x27to31bits+0+4(SB)/4,  $0x06050403
+DATA ·shuffleInt32x27to31bits+0+8(SB)/4,  $0x09080706
+DATA ·shuffleInt32x27to31bits+0+12(SB)/4, $0x0D0C0B0A
+
+DATA ·shuffleInt32x27to31bits+0+16(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+0+20(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+0+24(SB)/4, $0x0A808080
+DATA ·shuffleInt32x27to31bits+0+28(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x27to31bits+0+32(SB)/4, $0x800F0E0D
+DATA ·shuffleInt32x27to31bits+0+36(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+0+40(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+0+44(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x27to31bits+0+48(SB)/4, $0x00808080
+DATA ·shuffleInt32x27to31bits+0+52(SB)/4, $0x03020100
+DATA ·shuffleInt32x27to31bits+0+56(SB)/4, $0x07060504
+DATA ·shuffleInt32x27to31bits+0+60(SB)/4, $0x0A090807
+
+DATA ·shuffleInt32x27to31bits+0+64(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+0+68(SB)/4, $0x04808080
+DATA ·shuffleInt32x27to31bits+0+72(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+0+76(SB)/4, $0x80808080
+
+// 28 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,a,a,a,a,a]
+// 3: [a,a,a,a,b,b,b,b]
+// 4: [b,b,b,b,b,b,b,b]
+// 5: [b,b,b,b,b,b,b,b]
+// 6: [b,b,b,b,b,b,b,b]
+// 7: [c,c,c,c,c,c,c,c]
+// 8: [c,c,c,c,c,c,c,c]
+// 9: [c,c,c,c,c,c,c,c]
+// A: [c,c,c,c,d,d,d,d]
+// B: [d,d,d,d,d,d,d,d]
+// C: [d,d,d,d,d,d,d,d]
+// D: [d,d,d,d,d,d,d,d]
+// E: [e,e,e,e,e,e,e,e]
+// F: [e,e,e,e,e,e,e,e]
+// ---
+// 0: [e,e,e,e,e,e,e,e]
+// 1: [e,e,e,e,f,f,f,f]
+// 2: [f,f,f,f,f,f,f,f]
+// 3: [f,f,f,f,f,f,f,f]
+// 4: [f,f,f,f,f,f,f,f]
+// 5: [g,g,g,g,g,g,g,g]
+// 6: [g,g,g,g,g,g,g,g]
+// 7: [g,g,g,g,g,g,g,g]
+// 8: [g,g,g,g,h,h,h,h]
+// 9: [h,h,h,h,h,h,h,h]
+// A: [h,h,h,h,h,h,h,h]
+// B: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x27to31bits+80+0(SB)/4,  $0x03020100
+DATA ·shuffleInt32x27to31bits+80+4(SB)/4,  $0x06050403
+DATA ·shuffleInt32x27to31bits+80+8(SB)/4,  $0x0A090807
+DATA ·shuffleInt32x27to31bits+80+12(SB)/4, $0x0D0C0B0A
+
+DATA ·shuffleInt32x27to31bits+80+16(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+80+20(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+80+24(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+80+28(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x27to31bits+80+32(SB)/4, $0x80800F0E
+DATA ·shuffleInt32x27to31bits+80+36(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+80+40(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+80+44(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x27to31bits+80+48(SB)/4, $0x01008080
+DATA ·shuffleInt32x27to31bits+80+52(SB)/4, $0x04030201
+DATA ·shuffleInt32x27to31bits+80+56(SB)/4, $0x08070605
+DATA ·shuffleInt32x27to31bits+80+60(SB)/4, $0x0B0A0908
+
+DATA ·shuffleInt32x27to31bits+80+64(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+80+68(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+80+72(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+80+76(SB)/4, $0x80808080
+
+// 29 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,a,a,a,a,a]
+// 3: [a,a,a,a,a,b,b,b]
+// 4: [b,b,b,b,b,b,b,b]
+// 5: [b,b,b,b,b,b,b,b]
+// 6: [b,b,b,b,b,b,b,b]
+// 7: [b,b,c,c,c,c,c,c]
+// 8: [c,c,c,c,c,c,c,c]
+// 9: [c,c,c,c,c,c,c,c]
+// A: [c,c,c,c,c,c,c,d]
+// B: [d,d,d,d,d,d,d,d]
+// C: [d,d,d,d,d,d,d,d]
+// D: [d,d,d,d,d,d,d,d]
+// E: [d,d,d,d,e,e,e,e]
+// F: [e,e,e,e,e,e,e,e]
+// ---
+// 0: [e,e,e,e,e,e,e,e]
+// 1: [e,e,e,e,e,e,e,e]
+// 2: [e,f,f,f,f,f,f,f]
+// 3: [f,f,f,f,f,f,f,f]
+// 4: [f,f,f,f,f,f,f,f]
+// 5: [f,f,f,f,f,f,g,g]
+// 6: [g,g,g,g,g,g,g,g]
+// 7: [g,g,g,g,g,g,g,g]
+// 8: [g,g,g,g,g,g,g,g]
+// 9: [g,g,g,h,h,h,h,h]
+// A: [h,h,h,h,h,h,h,h]
+// B: [h,h,h,h,h,h,h,h]
+// C: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x27to31bits+160+0(SB)/4,  $0x03020100
+DATA ·shuffleInt32x27to31bits+160+4(SB)/4,  $0x06050403
+DATA ·shuffleInt32x27to31bits+160+8(SB)/4,  $0x0A090807
+DATA ·shuffleInt32x27to31bits+160+12(SB)/4, $0x0D0C0B0A
+
+DATA ·shuffleInt32x27to31bits+160+16(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+160+20(SB)/4, $0x07808080
+DATA ·shuffleInt32x27to31bits+160+24(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+160+28(SB)/4, $0x0E808080
+
+DATA ·shuffleInt32x27to31bits+160+32(SB)/4, $0x80800F0E
+DATA ·shuffleInt32x27to31bits+160+36(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+160+40(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+160+44(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x27to31bits+160+48(SB)/4, $0x01008080
+DATA ·shuffleInt32x27to31bits+160+52(SB)/4, $0x05040302
+DATA ·shuffleInt32x27to31bits+160+56(SB)/4, $0x08070605
+DATA ·shuffleInt32x27to31bits+160+60(SB)/4, $0x0C0B0A09
+
+DATA ·shuffleInt32x27to31bits+160+64(SB)/4, $0x02808080
+DATA ·shuffleInt32x27to31bits+160+68(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+160+72(SB)/4, $0x09808080
+DATA ·shuffleInt32x27to31bits+160+76(SB)/4, $0x80808080
+
+// 30 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,a,a,a,a,a]
+// 3: [a,a,a,a,a,a,b,b]
+// 4: [b,b,b,b,b,b,b,b]
+// 5: [b,b,b,b,b,b,b,b]
+// 6: [b,b,b,b,b,b,b,b]
+// 7: [b,b,b,b,c,c,c,c]
+// 8: [c,c,c,c,c,c,c,c]
+// 9: [c,c,c,c,c,c,c,c]
+// A: [c,c,c,c,c,c,c,c]
+// B: [c,c,d,d,d,d,d,d]
+// C: [d,d,d,d,d,d,d,d]
+// D: [d,d,d,d,d,d,d,d]
+// E: [d,d,d,d,d,d,d,d]
+// F: [e,e,e,e,e,e,e,e]
+// ---
+// 0: [e,e,e,e,e,e,e,e]
+// 1: [e,e,e,e,e,e,e,e]
+// 2: [e,e,e,e,e,e,f,f]
+// 3: [f,f,f,f,f,f,f,f]
+// 4: [f,f,f,f,f,f,f,f]
+// 5: [f,f,f,f,f,f,f,f]
+// 6: [f,f,f,f,g,g,g,g]
+// 7: [g,g,g,g,g,g,g,g]
+// 8: [g,g,g,g,g,g,g,g]
+// 9: [g,g,g,g,g,g,g,g]
+// A: [g,g,h,h,h,h,h,h]
+// B: [h,h,h,h,h,h,h,h]
+// C: [h,h,h,h,h,h,h,h]
+// D: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x27to31bits+240+0(SB)/4,  $0x03020100
+DATA ·shuffleInt32x27to31bits+240+4(SB)/4,  $0x06050403
+DATA ·shuffleInt32x27to31bits+240+8(SB)/4,  $0x0A090807
+DATA ·shuffleInt32x27to31bits+240+12(SB)/4, $0x0E0D0C0B
+
+DATA ·shuffleInt32x27to31bits+240+16(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+240+20(SB)/4, $0x07808080
+DATA ·shuffleInt32x27to31bits+240+24(SB)/4, $0x0B808080
+DATA ·shuffleInt32x27to31bits+240+28(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x27to31bits+240+32(SB)/4, $0x8080800F
+DATA ·shuffleInt32x27to31bits+240+36(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+240+40(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+240+44(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x27to31bits+240+48(SB)/4, $0x02010080
+DATA ·shuffleInt32x27to31bits+240+52(SB)/4, $0x05040302
+DATA ·shuffleInt32x27to31bits+240+56(SB)/4, $0x09080706
+DATA ·shuffleInt32x27to31bits+240+60(SB)/4, $0x0D0C0B0A
+
+DATA ·shuffleInt32x27to31bits+240+64(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+240+68(SB)/4, $0x06808080
+DATA ·shuffleInt32x27to31bits+240+72(SB)/4, $0x0A808080
+DATA ·shuffleInt32x27to31bits+240+76(SB)/4, $0x80808080
+
+// 31 bits => 32 bits
+// ------------------
+// 0: [a,a,a,a,a,a,a,a]
+// 1: [a,a,a,a,a,a,a,a]
+// 2: [a,a,a,a,a,a,a,a]
+// 3: [a,a,a,a,a,a,a,b]
+// 4: [b,b,b,b,b,b,b,b]
+// 5: [b,b,b,b,b,b,b,b]
+// 6: [b,b,b,b,b,b,b,b]
+// 7: [b,b,b,b,b,b,c,c]
+// 8: [c,c,c,c,c,c,c,c]
+// 9: [c,c,c,c,c,c,c,c]
+// A: [c,c,c,c,c,c,c,c]
+// B: [c,c,c,c,c,d,d,d]
+// C: [d,d,d,d,d,d,d,d]
+// D: [d,d,d,d,d,d,d,d]
+// E: [d,d,d,d,d,d,d,d]
+// F: [d,d,d,d,e,e,e,e]
+// ---
+// 0: [e,e,e,e,e,e,e,e]
+// 1: [e,e,e,e,e,e,e,e]
+// 2: [e,e,e,e,e,e,e,e]
+// 3: [e,e,e,f,f,f,f,f]
+// 4: [f,f,f,f,f,f,f,f]
+// 5: [f,f,f,f,f,f,f,f]
+// 6: [f,f,f,f,f,f,f,f]
+// 7: [f,f,g,g,g,g,g,g]
+// 8: [g,g,g,g,g,g,g,g]
+// 9: [g,g,g,g,g,g,g,g]
+// A: [g,g,g,g,g,g,g,g]
+// B: [g,h,h,h,h,h,h,h]
+// C: [h,h,h,h,h,h,h,h]
+// D: [h,h,h,h,h,h,h,h]
+// E: [h,h,h,h,h,h,h,h]
+// ...
+DATA ·shuffleInt32x27to31bits+320+0(SB)/4,  $0x03020100
+DATA ·shuffleInt32x27to31bits+320+4(SB)/4,  $0x06050403
+DATA ·shuffleInt32x27to31bits+320+8(SB)/4,  $0x0A090807
+DATA ·shuffleInt32x27to31bits+320+12(SB)/4, $0x0E0D0C0B
+
+DATA ·shuffleInt32x27to31bits+320+16(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+320+20(SB)/4, $0x07808080
+DATA ·shuffleInt32x27to31bits+320+24(SB)/4, $0x0B808080
+DATA ·shuffleInt32x27to31bits+320+28(SB)/4, $0x0F808080
+
+DATA ·shuffleInt32x27to31bits+320+32(SB)/4, $0x8080800F
+DATA ·shuffleInt32x27to31bits+320+36(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+320+40(SB)/4, $0x80808080
+DATA ·shuffleInt32x27to31bits+320+44(SB)/4, $0x80808080
+
+DATA ·shuffleInt32x27to31bits+320+48(SB)/4, $0x02010080
+DATA ·shuffleInt32x27to31bits+320+52(SB)/4, $0x06050403
+DATA ·shuffleInt32x27to31bits+320+56(SB)/4, $0x0A090807
+DATA ·shuffleInt32x27to31bits+320+60(SB)/4, $0x0E0D0C0B
+
+DATA ·shuffleInt32x27to31bits+320+64(SB)/4, $0x03808080
+DATA ·shuffleInt32x27to31bits+320+68(SB)/4, $0x07808080
+DATA ·shuffleInt32x27to31bits+320+72(SB)/4, $0x0B808080
+DATA ·shuffleInt32x27to31bits+320+76(SB)/4, $0x80808080
+
+// The RIGHT shifts to unpack 32 bits integers.
+//
+// The following formula was determined empirically as the expression which
+// generates shift values:
+//
+//      shift[i] = (i * bitWidth) % 8
+//
+GLOBL ·shiftRightInt32(SB), RODATA|NOPTR, $256
+
+DATA ·shiftRightInt32+0+0(SB)/4,  $0
+DATA ·shiftRightInt32+0+4(SB)/4,  $1
+DATA ·shiftRightInt32+0+8(SB)/4,  $2
+DATA ·shiftRightInt32+0+12(SB)/4, $3
+DATA ·shiftRightInt32+0+16(SB)/4, $4
+DATA ·shiftRightInt32+0+20(SB)/4, $5
+DATA ·shiftRightInt32+0+24(SB)/4, $6
+DATA ·shiftRightInt32+0+28(SB)/4, $7
+
+DATA ·shiftRightInt32+32+0(SB)/4,  $0
+DATA ·shiftRightInt32+32+4(SB)/4,  $2
+DATA ·shiftRightInt32+32+8(SB)/4,  $4
+DATA ·shiftRightInt32+32+12(SB)/4, $6
+DATA ·shiftRightInt32+32+16(SB)/4, $0
+DATA ·shiftRightInt32+32+20(SB)/4, $2
+DATA ·shiftRightInt32+32+24(SB)/4, $4
+DATA ·shiftRightInt32+32+28(SB)/4, $6
+
+DATA ·shiftRightInt32+64+0(SB)/4,  $0
+DATA ·shiftRightInt32+64+4(SB)/4,  $3
+DATA ·shiftRightInt32+64+8(SB)/4,  $6
+DATA ·shiftRightInt32+64+12(SB)/4, $1
+DATA ·shiftRightInt32+64+16(SB)/4, $4
+DATA ·shiftRightInt32+64+20(SB)/4, $7
+DATA ·shiftRightInt32+64+24(SB)/4, $2
+DATA ·shiftRightInt32+64+28(SB)/4, $5
+
+DATA ·shiftRightInt32+96+0(SB)/4,  $0
+DATA ·shiftRightInt32+96+4(SB)/4,  $4
+DATA ·shiftRightInt32+96+8(SB)/4,  $0
+DATA ·shiftRightInt32+96+12(SB)/4, $4
+DATA ·shiftRightInt32+96+16(SB)/4, $0
+DATA ·shiftRightInt32+96+20(SB)/4, $4
+DATA ·shiftRightInt32+96+24(SB)/4, $0
+DATA ·shiftRightInt32+96+28(SB)/4, $4
+
+DATA ·shiftRightInt32+128+0(SB)/4,  $0
+DATA ·shiftRightInt32+128+4(SB)/4,  $5
+DATA ·shiftRightInt32+128+8(SB)/4,  $2
+DATA ·shiftRightInt32+128+12(SB)/4, $7
+DATA ·shiftRightInt32+128+16(SB)/4, $4
+DATA ·shiftRightInt32+128+20(SB)/4, $1
+DATA ·shiftRightInt32+128+24(SB)/4, $6
+DATA ·shiftRightInt32+128+28(SB)/4, $3
+
+DATA ·shiftRightInt32+160+0(SB)/4,  $0
+DATA ·shiftRightInt32+160+4(SB)/4,  $6
+DATA ·shiftRightInt32+160+8(SB)/4,  $4
+DATA ·shiftRightInt32+160+12(SB)/4, $2
+DATA ·shiftRightInt32+160+16(SB)/4, $0
+DATA ·shiftRightInt32+160+20(SB)/4, $6
+DATA ·shiftRightInt32+160+24(SB)/4, $4
+DATA ·shiftRightInt32+160+28(SB)/4, $2
+
+DATA ·shiftRightInt32+192+0(SB)/4,  $0
+DATA ·shiftRightInt32+192+4(SB)/4,  $7
+DATA ·shiftRightInt32+192+8(SB)/4,  $6
+DATA ·shiftRightInt32+192+12(SB)/4, $5
+DATA ·shiftRightInt32+192+16(SB)/4, $4
+DATA ·shiftRightInt32+192+20(SB)/4, $3
+DATA ·shiftRightInt32+192+24(SB)/4, $2
+DATA ·shiftRightInt32+192+28(SB)/4, $1
+
+DATA ·shiftRightInt32+224+0(SB)/4,  $0
+DATA ·shiftRightInt32+224+4(SB)/4,  $0
+DATA ·shiftRightInt32+224+8(SB)/4,  $0
+DATA ·shiftRightInt32+224+12(SB)/4, $0
+DATA ·shiftRightInt32+224+16(SB)/4, $0
+DATA ·shiftRightInt32+224+20(SB)/4, $0
+DATA ·shiftRightInt32+224+24(SB)/4, $0
+DATA ·shiftRightInt32+224+28(SB)/4, $0
+
+// The LEFT shifts to unpack 32 bits integers.
+//
+// The following formula was determined empirically as the expression which
+// generates shift values:
+//
+//      shift[i] = (8 - (i * bitWidth)) % 8
+//
+GLOBL ·shiftLeftInt32(SB), RODATA|NOPTR, $256
+
+DATA ·shiftLeftInt32+0+0(SB)/4,  $0
+DATA ·shiftLeftInt32+0+4(SB)/4,  $7
+DATA ·shiftLeftInt32+0+8(SB)/4,  $6
+DATA ·shiftLeftInt32+0+12(SB)/4, $5
+DATA ·shiftLeftInt32+0+16(SB)/4, $4
+DATA ·shiftLeftInt32+0+20(SB)/4, $3
+DATA ·shiftLeftInt32+0+24(SB)/4, $2
+DATA ·shiftLeftInt32+0+28(SB)/4, $1
+
+DATA ·shiftLeftInt32+32+0(SB)/4,  $0
+DATA ·shiftLeftInt32+32+4(SB)/4,  $6
+DATA ·shiftLeftInt32+32+8(SB)/4,  $4
+DATA ·shiftLeftInt32+32+12(SB)/4, $2
+DATA ·shiftLeftInt32+32+16(SB)/4, $0
+DATA ·shiftLeftInt32+32+20(SB)/4, $6
+DATA ·shiftLeftInt32+32+24(SB)/4, $4
+DATA ·shiftLeftInt32+32+28(SB)/4, $2
+
+DATA ·shiftLeftInt32+64+0(SB)/4,  $0
+DATA ·shiftLeftInt32+64+4(SB)/4,  $5
+DATA ·shiftLeftInt32+64+8(SB)/4,  $2
+DATA ·shiftLeftInt32+64+12(SB)/4, $7
+DATA ·shiftLeftInt32+64+16(SB)/4, $4
+DATA ·shiftLeftInt32+64+20(SB)/4, $1
+DATA ·shiftLeftInt32+64+24(SB)/4, $6
+DATA ·shiftLeftInt32+64+28(SB)/4, $3
+
+DATA ·shiftLeftInt32+96+0(SB)/4,  $0
+DATA ·shiftLeftInt32+96+4(SB)/4,  $4
+DATA ·shiftLeftInt32+96+8(SB)/4,  $0
+DATA ·shiftLeftInt32+96+12(SB)/4, $4
+DATA ·shiftLeftInt32+96+16(SB)/4, $0
+DATA ·shiftLeftInt32+96+20(SB)/4, $4
+DATA ·shiftLeftInt32+96+24(SB)/4, $0
+DATA ·shiftLeftInt32+96+28(SB)/4, $4
+
+DATA ·shiftLeftInt32+128+0(SB)/4,  $0
+DATA ·shiftLeftInt32+128+4(SB)/4,  $3
+DATA ·shiftLeftInt32+128+8(SB)/4,  $6
+DATA ·shiftLeftInt32+128+12(SB)/4, $1
+DATA ·shiftLeftInt32+128+16(SB)/4, $4
+DATA ·shiftLeftInt32+128+20(SB)/4, $7
+DATA ·shiftLeftInt32+128+24(SB)/4, $2
+DATA ·shiftLeftInt32+128+28(SB)/4, $5
+
+DATA ·shiftLeftInt32+160+0(SB)/4,  $0
+DATA ·shiftLeftInt32+160+4(SB)/4,  $2
+DATA ·shiftLeftInt32+160+8(SB)/4,  $4
+DATA ·shiftLeftInt32+160+12(SB)/4, $6
+DATA ·shiftLeftInt32+160+16(SB)/4, $0
+DATA ·shiftLeftInt32+160+20(SB)/4, $2
+DATA ·shiftLeftInt32+160+24(SB)/4, $4
+DATA ·shiftLeftInt32+160+28(SB)/4, $6
+
+DATA ·shiftLeftInt32+192+0(SB)/4,  $0
+DATA ·shiftLeftInt32+192+4(SB)/4,  $1
+DATA ·shiftLeftInt32+192+8(SB)/4,  $2
+DATA ·shiftLeftInt32+192+12(SB)/4, $3
+DATA ·shiftLeftInt32+192+16(SB)/4, $4
+DATA ·shiftLeftInt32+192+20(SB)/4, $5
+DATA ·shiftLeftInt32+192+24(SB)/4, $6
+DATA ·shiftLeftInt32+192+28(SB)/4, $7
+
+DATA ·shiftLeftInt32+224+0(SB)/4,  $0
+DATA ·shiftLeftInt32+224+4(SB)/4,  $0
+DATA ·shiftLeftInt32+224+8(SB)/4,  $0
+DATA ·shiftLeftInt32+224+12(SB)/4, $0
+DATA ·shiftLeftInt32+224+16(SB)/4, $0
+DATA ·shiftLeftInt32+224+20(SB)/4, $0
+DATA ·shiftLeftInt32+224+24(SB)/4, $0
+DATA ·shiftLeftInt32+224+28(SB)/4, $0
diff --git a/internal/bitpack/pack.go b/internal/bitpack/pack.go
new file mode 100644
index 0000000..5852028
--- /dev/null
+++ b/internal/bitpack/pack.go
@@ -0,0 +1,83 @@
+package bitpack
+
+import (
+	"encoding/binary"
+)
+
+// PackInt32 packs values from src to dst, each value is packed into the given
+// bit width regardless of how many bits are needed to represent it.
+//
+// The function panics if dst is too short to hold the bit packed values.
+func PackInt32(dst []byte, src []int32, bitWidth uint) {
+	assertPack(dst, len(src), bitWidth)
+	packInt32(dst, src, bitWidth)
+}
+
+func packInt32(dst []byte, src []int32, bitWidth uint) {
+	n := ByteCount(uint(len(src)) * bitWidth)
+	b := dst[:n]
+
+	for i := range b {
+		b[i] = 0
+	}
+
+	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
+	}
+}
+
+// PackInt64 packs values from src to dst, each value is packed into the given
+// bit width regardless of how many bits are needed to represent it.
+//
+// The function panics if dst is too short to hold the bit packed values.
+func PackInt64(dst []byte, src []int64, bitWidth uint) {
+	assertPack(dst, len(src), bitWidth)
+	packInt64(dst, src, bitWidth)
+}
+
+func packInt64(dst []byte, src []int64, bitWidth uint) {
+	n := ByteCount(uint(len(src)) * bitWidth)
+	b := dst[:n]
+
+	for i := range b {
+		b[i] = 0
+	}
+
+	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
+	}
+}
+
+func assertPack(dst []byte, count int, bitWidth uint) {
+	_ = dst[:ByteCount(bitWidth*uint(count))]
+}
diff --git a/internal/bitpack/unpack.go b/internal/bitpack/unpack.go
new file mode 100644
index 0000000..0e948ea
--- /dev/null
+++ b/internal/bitpack/unpack.go
@@ -0,0 +1,27 @@
+package bitpack
+
+// Padding is the padding expected to exist after the end of input buffers for
+// the unpacking algorithms to avoid reading beyond the end of the input.
+const Padding = 16
+
+// UnpackInt32 unpacks 32 bit integers from src to dst.
+//
+// The function unpacked len(dst) integers, it panics if src is too short to
+// contain len(dst) values of the given bit width.
+func UnpackInt32(dst []int32, src []byte, bitWidth uint) {
+	assertUnpack(src, len(dst), bitWidth)
+	unpackInt32(dst, src, bitWidth)
+}
+
+// UnpackInt64 unpacks 64 bit integers from src to dst.
+//
+// The function unpacked len(dst) integers, it panics if src is too short to
+// contain len(dst) values of the given bit width.
+func UnpackInt64(dst []int64, src []byte, bitWidth uint) {
+	assertUnpack(src, len(dst), bitWidth)
+	unpackInt64(dst, src, bitWidth)
+}
+
+func assertUnpack(src []byte, count int, bitWidth uint) {
+	_ = src[:ByteCount(bitWidth*uint(count)+8*Padding)]
+}
diff --git a/internal/bitpack/unpack_int32_amd64.go b/internal/bitpack/unpack_int32_amd64.go
new file mode 100644
index 0000000..141c2be
--- /dev/null
+++ b/internal/bitpack/unpack_int32_amd64.go
@@ -0,0 +1,36 @@
+//go:build !purego
+
+package bitpack
+
+import (
+	"github.com/segmentio/parquet-go/internal/unsafecast"
+	"golang.org/x/sys/cpu"
+)
+
+//go:noescape
+func unpackInt32Default(dst []int32, src []byte, bitWidth uint)
+
+//go:noescape
+func unpackInt32x1to16bitsAVX2(dst []int32, src []byte, bitWidth uint)
+
+//go:noescape
+func unpackInt32x17to26bitsAVX2(dst []int32, src []byte, bitWidth uint)
+
+//go:noescape
+func unpackInt32x27to31bitsAVX2(dst []int32, src []byte, bitWidth uint)
+
+func unpackInt32(dst []int32, src []byte, bitWidth uint) {
+	hasAVX2 := cpu.X86.HasAVX2
+	switch {
+	case hasAVX2 && bitWidth <= 16:
+		unpackInt32x1to16bitsAVX2(dst, src, bitWidth)
+	case hasAVX2 && bitWidth <= 26:
+		unpackInt32x17to26bitsAVX2(dst, src, bitWidth)
+	case hasAVX2 && bitWidth <= 31:
+		unpackInt32x27to31bitsAVX2(dst, src, bitWidth)
+	case bitWidth == 32:
+		copy(dst, unsafecast.BytesToInt32(src))
+	default:
+		unpackInt32Default(dst, src, bitWidth)
+	}
+}
diff --git a/internal/bitpack/unpack_int32_amd64.s b/internal/bitpack/unpack_int32_amd64.s
new file mode 100644
index 0000000..94f5d86
--- /dev/null
+++ b/internal/bitpack/unpack_int32_amd64.s
@@ -0,0 +1,352 @@
+//go:build !purego
+
+#include "funcdata.h"
+#include "textflag.h"
+
+// func unpackInt32Default(dst []int32, src []uint32, bitWidth uint)
+TEXT ·unpackInt32Default(SB), NOSPLIT, $0-56
+    MOVQ dst_base+0(FP), AX
+    MOVQ dst_len+8(FP), DX
+    MOVQ src_base+24(FP), BX
+    MOVQ bitWidth+48(FP), CX
+
+    MOVQ $1, R8 // bitMask = (1 << bitWidth) - 1
+    SHLQ CX, R8
+    DECQ R8
+    MOVQ CX, R9 // bitWidth
+
+    XORQ DI, DI // bitOffset
+    XORQ SI, SI // index
+    JMP test
+loop:
+    MOVQ DI, R10
+    MOVQ DI, CX
+    SHRQ $5, R10      // i = bitOffset / 32
+    ANDQ $0b11111, CX // j = bitOffset % 32
+
+    MOVL (BX)(R10*4), R11
+    MOVL R8, R12  // d = bitMask
+    SHLL CX, R12  // d = d << j
+    ANDL R12, R11 // d = src[i] & d
+    SHRL CX, R11  // d = d >> j
+
+    MOVL CX, R13
+    ADDL R9, R13
+    CMPL R13, $32
+    JBE next // j+bitWidth <= 32 ?
+
+    MOVL 4(BX)(R10*4), R14
+    MOVL CX, R12
+    MOVL $32, CX
+    SUBL R12, CX  // k = 32 - j
+    MOVL R8, R12  // c = bitMask
+    SHRL CX, R12  // c = c >> k
+    ANDL R12, R14 // c = src[i+1] & c
+    SHLL CX, R14  // c = c << k
+    ORL R14, R11  // d = d | c
+next:
+    MOVL R11, (AX)(SI*4) // dst[n] = d
+    ADDQ R9, DI          // bitOffset += bitWidth
+    INCQ SI
+test:
+    CMPQ SI, DX
+    JNE loop
+    RET
+
+// -----------------------------------------------------------------------------
+// The unpack* functions below are adaptations of the algorithms
+// described in "Decoding billions of integers per second through vectorization"
+// from D. Lemire & L. Boytsov, the following changes were made:
+//
+// - The paper described two methods for decoding integers called "horizontal"
+//   and "vertical". The "horizontal" version is the one that applies the best
+//   to the bit packing done in the Parquet delta encoding; however, it also
+//   differs in some ways, many compression techniques discussed in the paper
+//   are not implemented in the Parquet format.
+//
+// - The paper focuses on implementations based on SSE instructions, which
+//   describes how to use PMULLD to emulate the lack of variable bit shift
+//   for packed integers. Our version of the bit unpacking algorithms here
+//   uses AVX2 and can perform variable bit shifts using VPSRLVD, which yields
+//   better throughput since the instruction latency is a single CPU cycle,
+//   vs 10 for VPMULLD.
+//
+// - The reference implementation at https://github.com/lemire/FastPFor/ uses
+//   specializations for each bit size, resulting in 32 unique functions.
+//   Our version here are more generic, we provide 3 variations of the
+//   algorithm for bit widths 1 to 16, 17 to 26, and 27 to 31 (unpacking 32
+//   bits values is a simple copy). In that regard, our implementation is
+//   somewhat an improvement over the reference, since it uses less code and
+//   less memory to hold the shuffle masks and shift tables.
+//
+// Technically, each specialization of our functions could be expressed by the
+// algorithm used for unpacking values of 27 to 31 bits. However, multiple steps
+// of the main loop can be removed for lower bit widths, providing up to ~35%
+// better throughput for smaller sizes. Since we expect delta encoding to often
+// result in bit packing values to smaller bit widths, the specializations are
+// worth the extra complexity.
+//
+// For more details, see: https://arxiv.org/pdf/1209.2137v5.pdf
+// -----------------------------------------------------------------------------
+
+// unpackInt32x1to16bitsAVX2 is the implementation of the bit unpacking
+// algorithm for inputs of bit width 1 to 16.
+//
+// In this version of the algorithm, we can perform a single memory load in each
+// loop iteration since we know that 8 values will fit in a single XMM register.
+//
+// func unpackInt32x1to16bitsAVX2(dst []int32, src []uint32, bitWidth uint)
+TEXT ·unpackInt32x1to16bitsAVX2(SB), NOSPLIT, $56-56
+    NO_LOCAL_POINTERS
+    MOVQ dst_base+0(FP), AX
+    MOVQ dst_len+8(FP), DX
+    MOVQ src_base+24(FP), BX
+    MOVQ bitWidth+48(FP), CX
+
+    CMPQ DX, $8
+    JB tail
+
+    MOVQ DX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+    XORQ SI, SI
+
+    MOVQ $1, R8
+    SHLQ CX, R8
+    DECQ R8
+    MOVQ R8, X0
+    VPBROADCASTD X0, X0
+
+    MOVQ CX, R9
+    DECQ R9
+    SHLQ $5, R9 // 32 * (bitWidth - 1)
+
+    MOVQ CX, R10
+    DECQ R10
+    SHLQ $5, R10
+    ANDQ $0xFF, R10 // (32 * (bitWidth - 1)) % 256
+
+    LEAQ ·shuffleInt32x1to16bits(SB), R11
+    VMOVDQA (R11)(R9*1), X1
+    VMOVDQA 16(R11)(R9*1), X2
+
+    LEAQ ·shiftRightInt32(SB), R12
+    VMOVDQA (R12)(R10*1), X3
+    VMOVDQA 16(R12)(R10*1), X4
+loop:
+    VMOVDQU (BX), X7
+
+    VPSHUFB X1, X7, X5
+    VPSHUFB X2, X7, X6
+
+    VPSRLVD X3, X5, X5
+    VPSRLVD X4, X6, X6
+
+    VPAND X0, X5, X5
+    VPAND X0, X6, X6
+
+    VMOVDQU X5, (AX)(SI*4)
+    VMOVDQU X6, 16(AX)(SI*4)
+
+    ADDQ CX, BX
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loop
+    VZEROUPPER
+
+    CMPQ SI, DX
+    JE done
+    LEAQ (AX)(SI*4), AX
+    SUBQ SI, DX
+tail:
+    MOVQ AX, dst_base-56(SP)
+    MOVQ DX, dst_len-48(SP)
+    MOVQ BX, src_base-32(SP)
+    MOVQ CX, bitWidth-8(SP)
+    CALL ·unpackInt32Default(SB)
+done:
+    RET
+
+// unpackInt32x17to26bitsAVX2 is the implementation of the bit unpacking
+// algorithm for inputs of bit width 17 to 26.
+//
+// In this version of the algorithm, we need to 32 bytes at each loop iteration
+// because 8 bit-packed values will span across two XMM registers.
+//
+// func unpackInt32x17to26bitsAVX2(dst []int32, src []uint32, bitWidth uint)
+TEXT ·unpackInt32x17to26bitsAVX2(SB), NOSPLIT, $56-56
+    NO_LOCAL_POINTERS
+    MOVQ dst_base+0(FP), AX
+    MOVQ dst_len+8(FP), DX
+    MOVQ src_base+24(FP), BX
+    MOVQ bitWidth+48(FP), CX
+
+    CMPQ DX, $8
+    JB tail
+
+    MOVQ DX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+    XORQ SI, SI
+
+    MOVQ $1, R8
+    SHLQ CX, R8
+    DECQ R8
+    MOVQ R8, X0
+    VPBROADCASTD X0, X0
+
+    MOVQ CX, R9
+    SUBQ $17, R9
+    IMULQ $48, R9 // 48 * (bitWidth - 17)
+
+    MOVQ CX, R10
+    DECQ R10
+    SHLQ $5, R10
+    ANDQ $0xFF, R10 // (32 * (bitWidth - 1)) % 256
+
+    LEAQ ·shuffleInt32x17to26bits(SB), R11
+    VMOVDQA (R11)(R9*1), X1
+    VMOVDQA 16(R11)(R9*1), X2
+    VMOVDQA 32(R11)(R9*1), X3
+
+    LEAQ ·shiftRightInt32(SB), R12
+    VMOVDQA (R12)(R10*1), X4
+    VMOVDQA 16(R12)(R10*1), X5
+loop:
+    VMOVDQU (BX), X6
+    VMOVDQU 16(BX), X7
+
+    VPSHUFB X1, X6, X8
+    VPSHUFB X2, X6, X9
+    VPSHUFB X3, X7, X10
+    VPOR X10, X9, X9
+
+    VPSRLVD X4, X8, X8
+    VPSRLVD X5, X9, X9
+
+    VPAND X0, X8, X8
+    VPAND X0, X9, X9
+
+    VMOVDQU X8, (AX)(SI*4)
+    VMOVDQU X9, 16(AX)(SI*4)
+
+    ADDQ CX, BX
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loop
+    VZEROUPPER
+
+    CMPQ SI, DX
+    JE done
+    LEAQ (AX)(SI*4), AX
+    SUBQ SI, DX
+tail:
+    MOVQ AX, dst_base-56(SP)
+    MOVQ DX, dst_len-48(SP)
+    MOVQ BX, src_base-32(SP)
+    MOVQ CX, bitWidth-8(SP)
+    CALL ·unpackInt32Default(SB)
+done:
+    RET
+
+// unpackInt32x27to31bitsAVX2 is the implementation of the bit unpacking
+// algorithm for inputs of bit width 27 to 31.
+//
+// In this version of the algorithm the bit-packed values may span across up to
+// 5 bytes. The simpler approach for smaller bit widths where we could perform a
+// single shuffle + shift to unpack the values do not work anymore.
+//
+// Values are unpacked in two steps: the first one extracts lower bits which are
+// shifted RIGHT to align on the beginning of 32 bit words, the second extracts
+// upper bits which are shifted LEFT to be moved to the end of the 32 bit words.
+//
+// The amount of LEFT shifts is always "8 minus the amount of RIGHT shift".
+//
+// func unpackInt32x27to31bitsAVX2(dst []int32, src []uint32, bitWidth uint)
+TEXT ·unpackInt32x27to31bitsAVX2(SB), NOSPLIT, $56-56
+    NO_LOCAL_POINTERS
+    MOVQ dst_base+0(FP), AX
+    MOVQ dst_len+8(FP), DX
+    MOVQ src_base+24(FP), BX
+    MOVQ bitWidth+48(FP), CX
+
+    CMPQ DX, $8
+    JB tail
+
+    MOVQ DX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+    XORQ SI, SI
+
+    MOVQ $1, R8
+    SHLQ CX, R8
+    DECQ R8
+    MOVQ R8, X0
+    VPBROADCASTD X0, X0
+
+    MOVQ CX, R9
+    SUBQ $27, R9
+    IMULQ $80, R9 // (80 * (bitWidth - 27))
+
+    MOVQ CX, R10
+    DECQ R10
+    SHLQ $5, R10
+    ANDQ $0xFF, R10 // (32 * (bitWidth - 1)) % 256
+
+    LEAQ ·shuffleInt32x27to31bits(SB), R11
+    VMOVDQA (R11)(R9*1), X1
+    VMOVDQA 16(R11)(R9*1), X2
+    VMOVDQA 32(R11)(R9*1), X3
+    VMOVDQA 48(R11)(R9*1), X4
+    VMOVDQA 64(R11)(R9*1), X5
+
+    LEAQ ·shiftRightInt32(SB), R12
+    LEAQ ·shiftLeftInt32(SB), R13
+    VMOVDQA (R12)(R10*1), X6
+    VMOVDQA (R13)(R10*1), X7
+    VMOVDQA 16(R12)(R10*1), X8
+    VMOVDQA 16(R13)(R10*1), X9
+loop:
+    VMOVDQU (BX), X10
+    VMOVDQU 16(BX), X11
+
+    VPSHUFB X1, X10, X12
+    VPSHUFB X2, X10, X13
+    VPSHUFB X3, X10, X14
+    VPSHUFB X4, X11, X15
+    VPSHUFB X5, X11, X11
+
+    VPSRLVD X6, X12, X12
+    VPSLLVD X7, X13, X13
+    VPSRLVD X8, X14, X14
+    VPSRLVD X8, X15, X15
+    VPSLLVD X9, X11, X11
+
+    VPOR X13, X12, X12
+    VPOR X15, X14, X14
+    VPOR X11, X14, X14
+
+    VPAND X0, X12, X12
+    VPAND X0, X14, X14
+
+    VMOVDQU X12, (AX)(SI*4)
+    VMOVDQU X14, 16(AX)(SI*4)
+
+    ADDQ CX, BX
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loop
+    VZEROUPPER
+
+    CMPQ SI, DX
+    JE done
+    LEAQ (AX)(SI*4), AX
+    SUBQ SI, DX
+tail:
+    MOVQ AX, dst_base-56(SP)
+    MOVQ DX, dst_len-48(SP)
+    MOVQ BX, src_base-32(SP)
+    MOVQ CX, bitWidth-8(SP)
+    CALL ·unpackInt32Default(SB)
+done:
+    RET
diff --git a/internal/bitpack/unpack_int32_purego.go b/internal/bitpack/unpack_int32_purego.go
new file mode 100644
index 0000000..bf6390b
--- /dev/null
+++ b/internal/bitpack/unpack_int32_purego.go
@@ -0,0 +1,25 @@
+//go:build purego || !amd64
+
+package bitpack
+
+import (
+	"github.com/segmentio/parquet-go/internal/unsafecast"
+)
+
+func unpackInt32(dst []int32, src []byte, bitWidth uint) {
+	bits := unsafecast.BytesToUint32(src)
+	bitMask := uint32(1<<bitWidth) - 1
+	bitOffset := uint(0)
+
+	for n := range dst {
+		i := bitOffset / 32
+		j := bitOffset % 32
+		d := (bits[i] & (bitMask << j)) >> j
+		if j+bitWidth > 32 {
+			k := 32 - j
+			d |= (bits[i+1] & (bitMask >> k)) << k
+		}
+		dst[n] = int32(d)
+		bitOffset += bitWidth
+	}
+}
diff --git a/internal/bitpack/unpack_int64_amd64.go b/internal/bitpack/unpack_int64_amd64.go
new file mode 100644
index 0000000..ac4b38a
--- /dev/null
+++ b/internal/bitpack/unpack_int64_amd64.go
@@ -0,0 +1,17 @@
+//go:build !purego
+
+package bitpack
+
+import "github.com/segmentio/parquet-go/internal/unsafecast"
+
+//go:noescape
+func unpackInt64Default(dst []int64, src []byte, bitWidth uint)
+
+func unpackInt64(dst []int64, src []byte, bitWidth uint) {
+	switch {
+	case bitWidth == 64:
+		copy(dst, unsafecast.BytesToInt64(src))
+	default:
+		unpackInt64Default(dst, src, bitWidth)
+	}
+}
diff --git a/internal/bitpack/unpack_int64_amd64.s b/internal/bitpack/unpack_int64_amd64.s
new file mode 100644
index 0000000..122ca27
--- /dev/null
+++ b/internal/bitpack/unpack_int64_amd64.s
@@ -0,0 +1,69 @@
+//go:build !purego
+
+#include "funcdata.h"
+#include "textflag.h"
+
+// func unpackInt64Default(dst []int64, src []uint32, bitWidth uint)
+TEXT ·unpackInt64Default(SB), NOSPLIT, $0-56
+    MOVQ dst_base+0(FP), AX
+    MOVQ dst_len+8(FP), DX
+    MOVQ src_base+24(FP), BX
+    MOVQ bitWidth+48(FP), CX
+
+    MOVQ $1, R8 // bitMask = (1 << bitWidth) - 1
+    SHLQ CX, R8, R8
+    DECQ R8
+    MOVQ CX, R9 // bitWidth
+
+    XORQ DI, DI // bitOffset
+    XORQ SI, SI // index
+    XORQ R10, R10
+    XORQ R11, R11
+    XORQ R14, R14
+    JMP test
+loop:
+    MOVQ DI, R10
+    MOVQ DI, CX
+    SHRQ $5, R10      // i = bitOffset / 32
+    ANDQ $0b11111, CX // j = bitOffset % 32
+
+    MOVLQZX (BX)(R10*4), R11
+    MOVQ R8, R12  // d = bitMask
+    SHLQ CX, R12  // d = d << j
+    ANDQ R12, R11 // d = src[i] & d
+    SHRQ CX, R11  // d = d >> j
+
+    MOVQ CX, R13
+    ADDQ R9, R13
+    CMPQ R13, $32
+    JBE next // j+bitWidth <= 32 ?
+    MOVQ CX, R15 // j
+
+    MOVLQZX 4(BX)(R10*4), R14
+    MOVQ $32, CX
+    SUBQ R15, CX  // k = 32 - j
+    MOVQ R8, R12  // c = bitMask
+    SHRQ CX, R12  // c = c >> k
+    ANDQ R12, R14 // c = src[i+1] & c
+    SHLQ CX, R14  // c = c << k
+    ORQ R14, R11  // d = d | c
+
+    CMPQ R13, $64
+    JBE next
+
+    MOVLQZX 8(BX)(R10*4), R14
+    MOVQ $64, CX
+    SUBQ R15, CX  // k = 64 - j
+    MOVQ R8, R12  // c = bitMask
+    SHRQ CX, R12  // c = c >> k
+    ANDQ R12, R14 // c = src[i+2] & c
+    SHLQ CX, R14  // c = c << k
+    ORQ R14, R11  // d = d | c
+next:
+    MOVQ R11, (AX)(SI*8) // dst[n] = d
+    ADDQ R9, DI          // bitOffset += bitWidth
+    INCQ SI
+test:
+    CMPQ SI, DX
+    JNE loop
+    RET
diff --git a/internal/bitpack/unpack_int64_purego.go b/internal/bitpack/unpack_int64_purego.go
new file mode 100644
index 0000000..2d0ece0
--- /dev/null
+++ b/internal/bitpack/unpack_int64_purego.go
@@ -0,0 +1,27 @@
+//go:build purego || !amd64
+
+package bitpack
+
+import "github.com/segmentio/parquet-go/internal/unsafecast"
+
+func unpackInt64(dst []int64, src []byte, bitWidth uint) {
+	bits := unsafecast.BytesToUint32(src)
+	bitMask := uint64(1<<bitWidth) - 1
+	bitOffset := uint(0)
+
+	for n := range dst {
+		i := bitOffset / 32
+		j := bitOffset % 32
+		d := (uint64(bits[i]) & (bitMask << j)) >> j
+		if j+bitWidth > 32 {
+			k := 32 - j
+			d |= (uint64(bits[i+1]) & (bitMask >> k)) << k
+			if j+bitWidth > 64 {
+				k := 64 - j
+				d |= (uint64(bits[i+2]) & (bitMask >> k)) << k
+			}
+		}
+		dst[n] = int64(d)
+		bitOffset += bitWidth
+	}
+}
diff --git a/internal/unsafecast/unsafecast_go17.go b/internal/unsafecast/unsafecast_go17.go
index 41dda85..82113a6 100644
--- a/internal/unsafecast/unsafecast_go17.go
+++ b/internal/unsafecast/unsafecast_go17.go
@@ -83,6 +83,10 @@ func Uint32ToInt32(data []uint32) []int32 {
 	return unsafe.Slice(*(**int32)(unsafe.Pointer(&data)), len(data))
 }
 
+func Uint32ToInt64(data []uint32) []int64 {
+	return unsafe.Slice(*(**int64)(unsafe.Pointer(&data)), len(data)/2)
+}
+
 func Uint64ToInt64(data []uint64) []int64 {
 	return unsafe.Slice(*(**int64)(unsafe.Pointer(&data)), len(data))
 }
diff --git a/internal/unsafecast/unsafecast_go18.go b/internal/unsafecast/unsafecast_go18.go
index 2ec70a8..fa869c3 100644
--- a/internal/unsafecast/unsafecast_go18.go
+++ b/internal/unsafecast/unsafecast_go18.go
@@ -140,6 +140,8 @@ func Float64ToUint64(data []float64) []uint64 { return Slice[uint64](data) }
 
 func Uint32ToInt32(data []uint32) []int32 { return Slice[int32](data) }
 
+func Uint32ToInt64(data []uint32) []int64 { return Slice[int64](data) }
+
 func Uint64ToInt64(data []uint64) []int64 { return Slice[int64](data) }
 
 func BytesToBool(data []byte) []bool { return Slice[bool](data) }
diff --git a/page.go b/page.go
index 302a7e5..9405144 100644
--- a/page.go
+++ b/page.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/segmentio/parquet-go/deprecated"
 	"github.com/segmentio/parquet-go/encoding/plain"
+	"github.com/segmentio/parquet-go/internal/bitpack"
 	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
@@ -466,7 +467,7 @@ type booleanPage struct {
 func newBooleanPage(typ Type, columnIndex int16, numValues int32, values []byte) *booleanPage {
 	return &booleanPage{
 		typ:         typ,
-		bits:        values[:byteCount(uint(numValues))],
+		bits:        values[:bitpack.ByteCount(uint(numValues))],
 		numValues:   numValues,
 		columnIndex: ^columnIndex,
 	}
diff --git a/parquet.go b/parquet.go
index 37dc0b5..e0cf8fb 100644
--- a/parquet.go
+++ b/parquet.go
@@ -31,7 +31,3 @@ func max(a, b int) int {
 	}
 	return b
 }
-
-func byteCount(numBits uint) int {
-	return int((numBits + 7) / 8)
-}
diff --git a/schema.go b/schema.go
index 95c789d..e4cac46 100644
--- a/schema.go
+++ b/schema.go
@@ -473,7 +473,7 @@ func makeStructField(f reflect.StructField) structField {
 		compressed = c
 	}
 
-	forEachStructTagOption(f.Tag, func(option, args string) {
+	forEachStructTagOption(f, func(t reflect.Type, option, args string) {
 		switch option {
 		case "optional":
 			setOptional()
@@ -503,19 +503,19 @@ func makeStructField(f reflect.StructField) structField {
 			setEncoding(&RLEDictionary)
 
 		case "delta":
-			switch f.Type.Kind() {
+			switch t.Kind() {
 			case reflect.Int, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint32, reflect.Uint64:
 				setEncoding(&DeltaBinaryPacked)
 			case reflect.String:
 				setEncoding(&DeltaByteArray)
 			case reflect.Slice:
-				if f.Type.Elem().Kind() == reflect.Uint8 { // []byte?
+				if t.Elem().Kind() == reflect.Uint8 { // []byte?
 					setEncoding(&DeltaByteArray)
 				} else {
 					throwInvalidFieldTag(f, option)
 				}
 			case reflect.Array:
-				if f.Type.Elem().Kind() == reflect.Uint8 { // [N]byte?
+				if t.Elem().Kind() == reflect.Uint8 { // [N]byte?
 					setEncoding(&DeltaByteArray)
 				} else {
 					throwInvalidFieldTag(f, option)
@@ -525,7 +525,7 @@ func makeStructField(f reflect.StructField) structField {
 			}
 
 		case "split":
-			switch f.Type.Kind() {
+			switch t.Kind() {
 			case reflect.Float32, reflect.Float64:
 				setEncoding(&ByteStreamSplit)
 			default:
@@ -533,9 +533,9 @@ func makeStructField(f reflect.StructField) structField {
 			}
 
 		case "list":
-			switch f.Type.Kind() {
+			switch t.Kind() {
 			case reflect.Slice:
-				element := nodeOf(f.Type.Elem())
+				element := nodeOf(t.Elem())
 				setNode(element)
 				setList()
 			default:
@@ -543,7 +543,7 @@ func makeStructField(f reflect.StructField) structField {
 			}
 
 		case "enum":
-			switch f.Type.Kind() {
+			switch t.Kind() {
 			case reflect.String:
 				setNode(Enum())
 			default:
@@ -551,9 +551,9 @@ func makeStructField(f reflect.StructField) structField {
 			}
 
 		case "uuid":
-			switch f.Type.Kind() {
+			switch t.Kind() {
 			case reflect.Array:
-				if f.Type.Elem().Kind() != reflect.Uint8 || f.Type.Len() != 16 {
+				if t.Elem().Kind() != reflect.Uint8 || t.Len() != 16 {
 					throwInvalidFieldTag(f, option)
 				}
 			default:
@@ -566,12 +566,12 @@ func makeStructField(f reflect.StructField) structField {
 				throwInvalidFieldTag(f, option+args)
 			}
 			var baseType Type
-			switch f.Type.Kind() {
+			switch t.Kind() {
 			case reflect.Int32:
 				baseType = Int32Type
 			case reflect.Int64:
 				baseType = Int64Type
-			case reflect.Array:
+			case reflect.Array, reflect.Slice:
 				baseType = FixedLenByteArrayType(decimalFixedLenByteArraySize(precision))
 			default:
 				throwInvalidFieldTag(f, option)
@@ -579,14 +579,14 @@ func makeStructField(f reflect.StructField) structField {
 
 			setNode(Decimal(scale, precision, baseType))
 		case "date":
-			switch f.Type.Kind() {
+			switch t.Kind() {
 			case reflect.Int32:
 				setNode(Date())
 			default:
 				throwInvalidFieldTag(f, option)
 			}
 		case "timestamp":
-			switch f.Type.Kind() {
+			switch t.Kind() {
 			case reflect.Int64:
 				timeUnit, err := parseTimestampArgs(args)
 				if err != nil {
@@ -630,14 +630,18 @@ func decimalFixedLenByteArraySize(precision int) int {
 	return int(math.Ceil((math.Log10(2) + float64(precision)) / math.Log10(256)))
 }
 
-func forEachStructTagOption(st reflect.StructTag, do func(option, args string)) {
-	if tag := st.Get("parquet"); tag != "" {
+func forEachStructTagOption(sf reflect.StructField, do func(t reflect.Type, option, args string)) {
+	if tag := sf.Tag.Get("parquet"); tag != "" {
 		_, tag = split(tag) // skip the field name
 		for tag != "" {
 			option := ""
 			option, tag = split(tag)
 			option, args := splitOptionArgs(option)
-			do(option, args)
+			ft := sf.Type
+			if ft.Kind() == reflect.Ptr {
+				ft = ft.Elem()
+			}
+			do(ft, option, args)
 		}
 	}
 }
diff --git a/value.go b/value.go
index a7e39e5..89eb621 100644
--- a/value.go
+++ b/value.go
@@ -257,6 +257,10 @@ func makeValue(k Kind, v reflect.Value) Value {
 			if v.Type().Elem().Kind() == reflect.Uint8 {
 				return makeValueFixedLenByteArray(v)
 			}
+		case reflect.Slice:
+			if v.Type().Elem().Kind() == reflect.Uint8 {
+				return makeValueBytes(k, v.Bytes())
+			}
 		}
 	}
 


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 02abb92..f5bd7db 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -32,6 +32,9 @@ jobs:
     - name: Run Tests
       run: go test -race -tags=${{ matrix.tags }} ./...
 
+    - name: Run Benchmarks
+      run: go test -short -tags=${{ matrix.tags }} -run '^$' -bench . -benchtime 1x ./...
+
   format:
     runs-on: ubuntu-latest
 
diff --git a/compress/compress_test.go b/compress/compress_test.go
index 5fae54d..e2a5754 100644
--- a/compress/compress_test.go
+++ b/compress/compress_test.go
@@ -144,7 +144,7 @@ func BenchmarkDecompressor(b *testing.B) {
 }
 
 func benchmarkZeroAllocsPerRun(b *testing.B, f func()) {
-	if allocs := testing.AllocsPerRun(b.N, f); allocs != 0 {
+	if allocs := testing.AllocsPerRun(b.N, f); allocs != 0 && !testing.Short() {
 		b.Errorf("too many memory allocations: %g > 0", allocs)
 	}
 }
diff --git a/encoding/delta/binary_packed_amd64_test.go b/encoding/delta/binary_packed_amd64_test.go
index d088531..74b5752 100644
--- a/encoding/delta/binary_packed_amd64_test.go
+++ b/encoding/delta/binary_packed_amd64_test.go
@@ -34,11 +34,11 @@ func TestBlockBitWidthsInt32AVX2(t *testing.T) {
 	testBlockBitWidthsInt32(t, blockBitWidthsInt32AVX2)
 }
 
-func TestMiniBlockPackInt32AVX2(t *testing.T) {
+func TestEncodeMiniBlockInt32AVX2(t *testing.T) {
 	requireAVX2(t)
-	testMiniBlockPackInt32(t,
+	testEncodeMiniBlockInt32(t,
 		func(dst []byte, src *[miniBlockSize]int32, bitWidth uint) {
-			miniBlockPackInt32AVX2(&dst[0], src, bitWidth)
+			encodeMiniBlockInt32AVX2(&dst[0], src, bitWidth)
 		},
 	)
 }
@@ -63,11 +63,11 @@ func BenchmarkBlockBitWidthsInt32AVX2(b *testing.B) {
 	benchmarkBlockBitWidthsInt32(b, blockBitWidthsInt32AVX2)
 }
 
-func BenchmarkMiniBlockPackInt32AVX2(b *testing.B) {
+func BenchmarkEncodeMiniBlockInt32AVX2(b *testing.B) {
 	requireAVX2(b)
-	benchmarkMiniBlockPackInt32(b,
+	benchmarkEncodeMiniBlockInt32(b,
 		func(dst []byte, src *[miniBlockSize]int32, bitWidth uint) {
-			miniBlockPackInt32AVX2(&dst[0], src, bitWidth)
+			encodeMiniBlockInt32AVX2(&dst[0], src, bitWidth)
 		},
 	)
 }
@@ -92,11 +92,11 @@ func TestBlockBitWidthsInt64AVX2(t *testing.T) {
 	testBlockBitWidthsInt64(t, blockBitWidthsInt64AVX2)
 }
 
-func TestMiniBlockPackInt64AVX2(t *testing.T) {
+func TestEncodeMiniBlockInt64AVX2(t *testing.T) {
 	requireAVX2(t)
-	testMiniBlockPackInt64(t,
+	testEncodeMiniBlockInt64(t,
 		func(dst []byte, src *[miniBlockSize]int64, bitWidth uint) {
-			miniBlockPackInt64AVX2(&dst[0], src, bitWidth)
+			encodeMiniBlockInt64AVX2(&dst[0], src, bitWidth)
 		},
 	)
 }
@@ -121,11 +121,11 @@ func BenchmarkBlockBitWidthsInt64AVX2(b *testing.B) {
 	benchmarkBlockBitWidthsInt64(b, blockBitWidthsInt64AVX2)
 }
 
-func BenchmarkMiniBlockPackInt64AVX2(b *testing.B) {
+func BenchmarkEncodeMiniBlockInt64AVX2(b *testing.B) {
 	requireAVX2(b)
-	benchmarkMiniBlockPackInt64(b,
+	benchmarkEncodeMiniBlockInt64(b,
 		func(dst []byte, src *[miniBlockSize]int64, bitWidth uint) {
-			miniBlockPackInt64AVX2(&dst[0], src, bitWidth)
+			encodeMiniBlockInt64AVX2(&dst[0], src, bitWidth)
 		},
 	)
 }
diff --git a/encoding/delta/binary_packed_purego_test.go b/encoding/delta/binary_packed_purego_test.go
deleted file mode 100644
index 80e75c4..0000000
--- a/encoding/delta/binary_packed_purego_test.go
+++ /dev/null
@@ -1,388 +0,0 @@
-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
index 877b819..d31b046 100644
--- a/encoding/delta/binary_packed_test.go
+++ b/encoding/delta/binary_packed_test.go
@@ -1,42 +1,388 @@
-//go:build amd64 && go1.18
-
-package delta_test
+package delta
 
 import (
+	"bytes"
 	"fmt"
+	"math/bits"
 	"testing"
-
-	"github.com/segmentio/parquet-go/encoding/delta"
-	"github.com/segmentio/parquet-go/encoding/test"
 )
 
-const (
-	encodeMinNumValues = 0
-	encodeMaxNumValues = 200
-)
+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 TestEncodeInt32(t *testing.T) {
-	for bitWidth := uint(0); bitWidth <= 32; bitWidth++ {
+func TestEncodeMiniBlockInt32(t *testing.T) {
+	testEncodeMiniBlockInt32(t, encodeMiniBlockInt32)
+}
+
+func testEncodeMiniBlockInt32(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) {
-			test.EncodeInt32(t,
-				new(delta.BinaryPackedEncoding),
-				encodeMinNumValues,
-				encodeMaxNumValues,
-				bitWidth,
-			)
+			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 BenchmarkEncodeMiniBlockInt32(b *testing.B) {
+	benchmarkEncodeMiniBlockInt32(b, encodeMiniBlockInt32)
+}
+
+func benchmarkEncodeMiniBlockInt32(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 TestEncodeInt64(t *testing.T) {
-	for bitWidth := uint(0); bitWidth <= 64; 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 TestEncodeMiniBlockInt64(t *testing.T) {
+	testEncodeMiniBlockInt64(t, encodeMiniBlockInt64)
+}
+
+func testEncodeMiniBlockInt64(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) {
-			test.EncodeInt64(t,
-				new(delta.BinaryPackedEncoding),
-				encodeMinNumValues,
-				encodeMaxNumValues,
-				bitWidth,
-			)
+			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 BenchmarkEncodeMiniBlockInt64(b *testing.B) {
+	benchmarkEncodeMiniBlockInt64(b, encodeMiniBlockInt64)
+}
+
+func benchmarkEncodeMiniBlockInt64(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/delta_test.go b/encoding/delta/delta_test.go
index 3d3b2fa..89b1f0d 100644
--- a/encoding/delta/delta_test.go
+++ b/encoding/delta/delta_test.go
@@ -4,10 +4,12 @@
 package delta_test
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/segmentio/parquet-go/encoding/delta"
 	"github.com/segmentio/parquet-go/encoding/fuzz"
+	"github.com/segmentio/parquet-go/encoding/test"
 )
 
 func FuzzDeltaBinaryPackedInt32(f *testing.F) {
@@ -25,3 +27,34 @@ func FuzzDeltaLengthByteArray(f *testing.F) {
 func FuzzDeltaByteArray(f *testing.F) {
 	fuzz.EncodeByteArray(f, new(delta.ByteArrayEncoding))
 }
+
+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/length_byte_array_test.go b/encoding/delta/length_byte_array_test.go
new file mode 100644
index 0000000..c7c14b6
--- /dev/null
+++ b/encoding/delta/length_byte_array_test.go
@@ -0,0 +1,73 @@
+package delta
+
+import (
+	"fmt"
+	"math/rand"
+	"testing"
+
+	"github.com/segmentio/parquet-go/encoding/plain"
+)
+
+func TestDecodeLengthByteArray(t *testing.T) {
+	const characters = "1234567890qwertyuiopasdfghjklzxcvbnm"
+	const numValues = 1000
+
+	src := []byte{}
+	dst := []byte{}
+	lengths := []int32{}
+
+	for i := 0; i < numValues; i++ {
+		n := i % len(characters)
+		src = append(src, characters[:n]...)
+		lengths = append(lengths, int32(n))
+	}
+
+	dst, err := decodeLengthByteArray(dst, src, lengths)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	index := 0
+	err = plain.RangeByteArrays(dst, func(got []byte) error {
+		want := characters[:index%len(characters)]
+
+		if want != string(got) {
+			return fmt.Errorf("wrong value at index %d: want=%q got=%q", index, want, got)
+		}
+
+		index++
+		return nil
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func BenchmarkDecodeLengthByteArray(b *testing.B) {
+	const padding = 64
+
+	for _, maxLen := range []int{0, 10, 20, 100, 1000} {
+		b.Run(fmt.Sprintf("maxLen=%d", maxLen), func(b *testing.B) {
+			lengths := make([]int32, 1000)
+			totalLength := 0
+			prng := rand.New(rand.NewSource(int64(maxLen)))
+
+			if maxLen > 0 {
+				for i := range lengths {
+					lengths[i] = prng.Int31n(int32(maxLen)) + 1
+					totalLength += int(lengths[i])
+				}
+			}
+
+			size := plain.ByteArrayLengthSize*len(lengths) + totalLength
+			dst := make([]byte, size+padding)
+			src := make([]byte, totalLength)
+			b.SetBytes(int64(size))
+			b.ResetTimer()
+
+			for i := 0; i < b.N; i++ {
+				dst, _ = decodeLengthByteArray(dst[:0], src, lengths)
+			}
+		})
+	}
+}
diff --git a/encoding/encoding_test.go b/encoding/encoding_test.go
index 800f42f..8c5425d 100644
--- a/encoding/encoding_test.go
+++ b/encoding/encoding_test.go
@@ -188,6 +188,8 @@ var fixedLenByteArrayTests = [...]struct {
 	{size: 2, data: []byte("ABCDEFGH")},
 	{size: 4, data: []byte("ABCDEFGH")},
 	{size: 8, data: []byte("ABCDEFGH")},
+	{size: 10, data: bytes.Repeat([]byte("123456789"), 100)},
+	{size: 16, data: bytes.Repeat([]byte("1234567890"), 160)},
 }
 
 var encodings = [...]encoding.Encoding{
@@ -825,7 +827,7 @@ func benchmarkDecodeFixedLenByteArray(b *testing.B, e encoding.Encoding) {
 }
 
 func benchmarkZeroAllocsPerRun(b *testing.B, f func()) {
-	if allocs := testing.AllocsPerRun(b.N, f); allocs != 0 {
+	if allocs := testing.AllocsPerRun(b.N, f); allocs != 0 && !testing.Short() {
 		b.Errorf("too many memory allocations: %g", allocs)
 	}
 }
@@ -885,13 +887,14 @@ func generateDoubleValues(n int, r *rand.Rand) []byte {
 }
 
 func generateByteArrayValues(n int, r *rand.Rand) []byte {
-	values := make([]byte, n*21)
+	const maxLen = 21
+	values := make([]byte, plain.ByteArrayLengthSize*n+n*maxLen)
 	length := 0
 
 	for i := 0; i < n; i++ {
-		k := r.Intn(20) + 1
+		k := r.Intn(maxLen) + 1
 		plain.PutByteArrayLength(values[length:], k)
-		length += 4
+		length += plain.ByteArrayLengthSize
 		io.ReadFull(r, values[length:length+k])
 		length += k
 	}
diff --git a/internal/bitpack/unpack_test.go b/internal/bitpack/unpack_test.go
new file mode 100644
index 0000000..b906aec
--- /dev/null
+++ b/internal/bitpack/unpack_test.go
@@ -0,0 +1,118 @@
+package bitpack_test
+
+import (
+	"fmt"
+	"math/rand"
+	"reflect"
+	"testing"
+
+	"github.com/segmentio/parquet-go/internal/bitpack"
+)
+
+const (
+	blockSize = 128
+)
+
+func TestUnpackInt32(t *testing.T) {
+	for bitWidth := uint(1); bitWidth <= 32; bitWidth++ {
+		t.Run(fmt.Sprintf("bitWidth=%d", bitWidth), func(t *testing.T) {
+			block := [blockSize]int32{}
+			bitMask := int32(bitWidth<<1) - 1
+
+			prng := rand.New(rand.NewSource(0))
+			for i := range block {
+				block[i] = prng.Int31() & bitMask
+			}
+
+			size := (blockSize * bitWidth) / 8
+			buf := make([]byte, size+bitpack.Padding)
+			bitpack.PackInt32(buf, block[:], bitWidth)
+
+			src := buf[:size]
+			dst := make([]int32, blockSize)
+
+			for n := 1; n <= blockSize; n++ {
+				for i := range dst {
+					dst[i] = 0
+				}
+
+				bitpack.UnpackInt32(dst[:n], src, bitWidth)
+
+				if !reflect.DeepEqual(block[:n], dst[:n]) {
+					t.Errorf("values mismatch for length=%d\nwant: %v\ngot:  %v", n, block[:n], dst[:n])
+				}
+			}
+		})
+	}
+}
+
+func TestUnpackInt64(t *testing.T) {
+	for bitWidth := uint(1); bitWidth <= 63; bitWidth++ {
+		t.Run(fmt.Sprintf("bitWidth=%d", bitWidth), func(t *testing.T) {
+			block := [blockSize]int64{}
+			bitMask := int64(bitWidth<<1) - 1
+
+			prng := rand.New(rand.NewSource(0))
+			for i := range block {
+				block[i] = prng.Int63() & bitMask
+			}
+
+			size := (blockSize * bitWidth) / 8
+			buf := make([]byte, size+bitpack.Padding)
+			bitpack.PackInt64(buf, block[:], bitWidth)
+
+			src := buf[:size]
+			dst := make([]int64, blockSize)
+
+			for n := 1; n <= blockSize; n++ {
+				for i := range dst {
+					dst[i] = 0
+				}
+
+				bitpack.UnpackInt64(dst[:n], src, bitWidth)
+
+				if !reflect.DeepEqual(block[:n], dst[:n]) {
+					t.Errorf("values mismatch for length=%d\nwant: %v\ngot:  %v", n, block[:n], dst[:n])
+				}
+			}
+		})
+	}
+}
+
+func BenchmarkUnpackInt32(b *testing.B) {
+	for bitWidth := uint(1); bitWidth <= 32; bitWidth++ {
+		block := [blockSize]int32{}
+		buf := [4*blockSize + bitpack.Padding]byte{}
+		bitpack.PackInt32(buf[:], block[:], bitWidth)
+
+		b.Run(fmt.Sprintf("bitWidth=%d", bitWidth), func(b *testing.B) {
+			dst := block[:]
+			src := buf[:]
+
+			for i := 0; i < b.N; i++ {
+				bitpack.UnpackInt32(dst, src, bitWidth)
+			}
+
+			b.SetBytes(4 * blockSize)
+		})
+	}
+}
+
+func BenchmarkUnpackInt64(b *testing.B) {
+	for bitWidth := uint(1); bitWidth <= 64; bitWidth++ {
+		block := [blockSize]int64{}
+		buf := [8*blockSize + bitpack.Padding]byte{}
+		bitpack.PackInt64(buf[:], block[:], bitWidth)
+
+		b.Run(fmt.Sprintf("bitWidth=%d", bitWidth), func(b *testing.B) {
+			dst := block[:]
+			src := buf[:]
+
+			for i := 0; i < b.N; i++ {
+				bitpack.UnpackInt64(dst, src, bitWidth)
+			}
+
+			b.SetBytes(4 * blockSize)
+		})
+	}
+}
diff --git a/merge_test.go b/merge_test.go
index 52db9d3..4d877d5 100644
--- a/merge_test.go
+++ b/merge_test.go
@@ -5,11 +5,9 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"math"
 	"math/rand"
 	"sort"
 	"testing"
-	"time"
 
 	"github.com/segmentio/parquet-go"
 )
@@ -43,7 +41,6 @@ func BenchmarkMergeRowGroups(b *testing.B) {
 					if err != nil {
 						b.Fatal(err)
 					}
-					start := time.Now()
 
 					rows := mergedRowGroup.Rows()
 					rbuf := make([]parquet.Row, benchmarkRowsPerStep)
@@ -60,9 +57,6 @@ func BenchmarkMergeRowGroups(b *testing.B) {
 						}
 						return n
 					})
-
-					seconds := time.Since(start).Seconds()
-					b.ReportMetric(float64(b.N)/seconds, "row/s")
 				})
 			}
 		})
@@ -115,7 +109,6 @@ func BenchmarkMergeFiles(b *testing.B) {
 					if err != nil {
 						b.Fatal(err)
 					}
-					start := time.Now()
 
 					rows := mergedRowGroup.Rows()
 					rbuf := make([]parquet.Row, benchmarkRowsPerStep)
@@ -137,10 +130,6 @@ func BenchmarkMergeFiles(b *testing.B) {
 					for _, f := range files[:n] {
 						totalSize += f.Size()
 					}
-
-					seconds := time.Since(start).Seconds()
-					b.ReportMetric(float64(b.N)/seconds, "row/s")
-					b.SetBytes(int64(math.Ceil(float64(totalSize) / benchmarkNumRows)))
 				})
 			}
 		})