Repository: segmentio/parquet-go
Pull request #229 (2022-06-10 23:32:42)

Optimize delta decoding (part 1)
This PR contributes to https://github.com/segmentio/parquet-go/issues/226 by optimizing delta decoding.

```
name                                             old time/op   new time/op    delta
Decode/DELTA_BINARY_PACKED/int32                   257µs ± 0%      20µs ± 0%    -92.38%  (p=0.000 n=9+10)
Decode/DELTA_BINARY_PACKED/int64                   257µs ± 0%      24µs ± 0%    -90.81%  (p=0.000 n=9+10)
Decode/DELTA_LENGTH_BYTE_ARRAY/byte_array          287µs ±14%      90µs ± 2%    -68.70%  (p=0.000 n=10+10)
Decode/DELTA_BYTE_ARRAY/byte_array                 470µs ±17%     156µs ± 1%    -66.73%  (p=0.000 n=10+10)
Decode/DELTA_BYTE_ARRAY/fixed_length_byte_array    265µs ±10%      92µs ± 0%    -65.40%  (p=0.000 n=8+9)

name                                             old speed     new speed      delta
Decode/DELTA_BINARY_PACKED/int32                 156MB/s ± 0%  2043MB/s ± 0%  +1211.86%  (p=0.000 n=9+10)
Decode/DELTA_BINARY_PACKED/int64                 311MB/s ± 0%  3387MB/s ± 0%   +988.35%  (p=0.000 n=9+10)
Decode/DELTA_LENGTH_BYTE_ARRAY/byte_array        506MB/s ±13%  1613MB/s ± 2%   +218.53%  (p=0.000 n=10+10)
Decode/DELTA_BYTE_ARRAY/byte_array               310MB/s ±15%   925MB/s ± 1%   +198.66%  (p=0.000 n=10+10)
Decode/DELTA_BYTE_ARRAY/fixed_length_byte_array  588MB/s ±24%  1747MB/s ± 0%   +197.23%  (p=0.000 n=9+9)

name                                             old value/s   new value/s    delta
Decode/DELTA_BINARY_PACKED/int32                   38.9M ± 0%    510.7M ± 0%  +1211.87%  (p=0.000 n=9+10)
Decode/DELTA_BINARY_PACKED/int64                   38.9M ± 0%    423.5M ± 0%   +988.36%  (p=0.000 n=9+10)
Decode/DELTA_LENGTH_BYTE_ARRAY/byte_array          35.0M ±13%    111.6M ± 2%   +218.53%  (p=0.000 n=10+10)
Decode/DELTA_BYTE_ARRAY/byte_array                 21.4M ±15%     64.0M ± 1%   +198.67%  (p=0.000 n=10+10)
Decode/DELTA_BYTE_ARRAY/fixed_length_byte_array    36.7M ±24%    109.2M ± 0%   +197.24%  (p=0.000 n=9+9)
```

Here is an example of the impact it has on the merge benchmarks:
```
name                                 old time/op  new time/op  delta
MergeFiles/UUID/groups=3,rows=30000  42.0µs ± 1%  20.0µs ± 4%   -52.47%  (p=0.000 n=10+10)

name                                 old row/s    new row/s    delta
MergeFiles/UUID/groups=3,rows=30000   23.4M ± 1%   49.3M ± 4%  +110.47%  (p=0.000 n=10+10)
```

There is more room for improvement by adding specializations for decoding bit-packed values of various bit widths.


Patch:
diff --git a/column_buffer.go b/column_buffer.go
index 80b81e7..9e9f43a 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"
 )
 
@@ -816,7 +817,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 +891,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/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..9f1763c 100644
--- a/encoding/rle/rle_amd64.go
+++ b/encoding/rle/rle_amd64.go
@@ -43,3 +43,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/file.go b/file.go
index eff129b..0302b26 100644
--- a/file.go
+++ b/file.go
@@ -541,9 +541,6 @@ func (f *filePages) readDictionaryPage(header *format.PageHeader, page *dataPage
 	if header.DictionaryPageHeader == nil {
 		return ErrMissingPageHeader
 	}
-	if f.index > 0 {
-		return ErrUnexpectedDictionaryPage
-	}
 	f.dictPage, _ = dictPagePool.Get().(*dictPage)
 	if f.dictPage == nil {
 		f.dictPage = new(dictPage)
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)
-}


Test patch:
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..c86c1e0 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{
@@ -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)))
 				})
 			}
 		})
diff --git a/reader_test.go b/reader_test.go
index 78a5493..ddd5f48 100644
--- a/reader_test.go
+++ b/reader_test.go
@@ -460,3 +460,41 @@ func TestSeekToRowDictReadSecond(t *testing.T) {
 		t.Fatalf("read != write")
 	}
 }
+
+func TestSeekToRowDictReadMultiplePages(t *testing.T) {
+	type rowType struct {
+		Name utf8string `parquet:",dict"`
+	}
+
+	// write samples to in-memory buffer
+	buf := new(bytes.Buffer)
+	schema := parquet.SchemaOf(new(rowType))
+	w := parquet.NewWriter(buf, schema, &parquet.WriterConfig{
+		PageBufferSize: 10,
+	})
+	sample := rowType{
+		Name: "foo1",
+	}
+
+	// write enough rows to spill over a single page
+	for i := 0; i < 10; i++ {
+		w.Write(sample)
+	}
+	sample.Name = "foo2"
+	w.Write(sample)
+	w.Close()
+
+	// create reader
+	r := parquet.NewReader(bytes.NewReader(buf.Bytes()))
+
+	// read 11th row
+	r.SeekToRow(10)
+	row := new(rowType)
+	err := r.Read(row)
+	if err != nil {
+		t.Fatalf("reading row: %v", err)
+	}
+	if *row != sample {
+		t.Fatalf("read != write")
+	}
+}