Repository: segmentio/parquet-go
Pull request #214 (2022-06-06 21:20:30)

Optimize dictionary lookups
This PR adds optimizations for dictionary lookups when AVX-512 is available.

The following benchmarks compare between the pure Go and SIMD-optimized versions of the code, looking 1000 indexes from the dictionary at each iteration:
```
name                                        old time/op  new time/op  delta
Dictionary/Bounds/BOOLEAN                   29.3ns ± 0%  29.3ns ± 0%      ~     (p=0.329 n=10+10)
Dictionary/Bounds/INT32                      773ns ± 0%   238ns ± 0%   -69.22%  (p=0.000 n=9+10)
Dictionary/Bounds/INT64                      775ns ± 0%   317ns ± 1%   -59.12%  (p=0.000 n=10+9)
Dictionary/Bounds/INT96                     6.59µs ± 1%  6.28µs ± 1%    -4.76%  (p=0.000 n=10+10)
Dictionary/Bounds/FLOAT                     1.22µs ± 0%  0.24µs ± 0%   -80.39%  (p=0.000 n=8+10)
Dictionary/Bounds/DOUBLE                    1.22µs ± 0%  0.31µs ± 0%   -74.48%  (p=0.000 n=9+9)
Dictionary/Bounds/BYTE_ARRAY                10.2µs ± 0%   7.8µs ± 0%   -23.83%  (p=0.000 n=9+9)
Dictionary/Bounds/FIXED_LEN_BYTE_ARRAY(16)  8.33µs ± 0%  7.50µs ± 1%    -9.94%  (p=0.000 n=9+10)
Dictionary/Bounds/INT(32,false)              891ns ± 0%   237ns ± 0%   -73.44%  (p=0.000 n=10+10)
Dictionary/Bounds/INT(64,false)              892ns ± 0%   318ns ± 0%   -64.37%  (p=0.000 n=9+10)
Dictionary/Lookup/BOOLEAN                   2.32µs ± 0%  1.75µs ± 0%   -24.60%  (p=0.000 n=9+9)
Dictionary/Lookup/INT32                     1.40µs ± 0%  0.62µs ± 2%   -55.66%  (p=0.000 n=9+9)
Dictionary/Lookup/INT64                     1.60µs ± 0%  0.64µs ± 0%   -59.95%  (p=0.000 n=9+8)
Dictionary/Lookup/INT96                     20.4µs ± 2%  19.8µs ± 2%    -3.05%  (p=0.000 n=9+9)
Dictionary/Lookup/FLOAT                     1.53µs ± 0%  0.63µs ± 1%   -59.15%  (p=0.000 n=10+9)
Dictionary/Lookup/DOUBLE                    1.43µs ± 0%  0.65µs ± 4%   -54.87%  (p=0.000 n=8+10)
Dictionary/Lookup/BYTE_ARRAY                3.58µs ± 0%  0.98µs ± 1%   -72.51%  (p=0.000 n=8+9)
Dictionary/Lookup/FIXED_LEN_BYTE_ARRAY(16)  2.68µs ± 1%  0.91µs ± 0%   -65.95%  (p=0.000 n=10+9)
Dictionary/Lookup/INT(32,false)             1.41µs ± 0%  0.61µs ± 1%   -56.85%  (p=0.000 n=10+9)
Dictionary/Lookup/INT(64,false)             1.58µs ± 0%  0.73µs ± 0%   -53.53%  (p=0.000 n=8+10)

name                                        old value/s  new value/s  delta
Dictionary/Bounds/BOOLEAN                    34.1G ± 0%   34.1G ± 0%      ~     (p=0.353 n=10+10)
Dictionary/Bounds/INT32                      1.29G ± 0%   4.20G ± 0%  +224.88%  (p=0.000 n=9+10)
Dictionary/Bounds/INT64                      1.29G ± 0%   3.16G ± 1%  +144.60%  (p=0.000 n=10+9)
Dictionary/Bounds/INT96                       152M ± 1%    159M ± 1%    +5.00%  (p=0.000 n=10+10)
Dictionary/Bounds/FLOAT                       820M ± 0%   4181M ± 0%  +409.82%  (p=0.000 n=9+10)
Dictionary/Bounds/DOUBLE                      818M ± 0%   3205M ± 0%  +291.93%  (p=0.000 n=9+9)
Dictionary/Bounds/BYTE_ARRAY                 98.0M ± 0%  128.6M ± 0%   +31.29%  (p=0.000 n=9+9)
Dictionary/Bounds/FIXED_LEN_BYTE_ARRAY(16)    120M ± 0%    133M ± 1%   +11.03%  (p=0.000 n=9+10)
Dictionary/Bounds/INT(32,false)              1.12G ± 0%   4.22G ± 0%  +276.45%  (p=0.000 n=10+10)
Dictionary/Bounds/INT(64,false)              1.12G ± 0%   3.15G ± 0%  +180.65%  (p=0.000 n=9+10)
Dictionary/Lookup/BOOLEAN                     432M ± 0%    573M ± 0%   +32.63%  (p=0.000 n=9+9)
Dictionary/Lookup/INT32                       715M ± 0%   1613M ± 2%  +125.54%  (p=0.000 n=9+9)
Dictionary/Lookup/INT64                       627M ± 0%   1565M ± 0%  +149.70%  (p=0.000 n=9+8)
Dictionary/Lookup/INT96                      49.0M ± 2%   50.5M ± 2%    +3.15%  (p=0.000 n=9+9)
Dictionary/Lookup/FLOAT                       652M ± 0%   1596M ± 1%  +144.83%  (p=0.000 n=10+9)
Dictionary/Lookup/DOUBLE                      699M ± 0%   1549M ± 4%  +121.69%  (p=0.000 n=8+10)
Dictionary/Lookup/BYTE_ARRAY                  280M ± 0%   1017M ± 1%  +263.77%  (p=0.000 n=8+9)
Dictionary/Lookup/FIXED_LEN_BYTE_ARRAY(16)    373M ± 1%   1096M ± 0%  +193.64%  (p=0.000 n=10+9)
Dictionary/Lookup/INT(32,false)               707M ± 0%   1639M ± 1%  +131.73%  (p=0.000 n=10+9)
Dictionary/Lookup/INT(64,false)               632M ± 0%   1361M ± 0%  +115.32%  (p=0.000 n=9+10)
```

achille-roussel: For the sake of completeness, here's a snapshot of benchmarks comparing the current purego and amd64 versions of the code:

```
name                                        old time/op  new time/op  delta
Dictionary/Bounds/BOOLEAN                   29.3ns ± 0%  29.3ns ± 0%      ~     (p=0.481 n=10+10)
Dictionary/Bounds/INT32                      774ns ± 0%   238ns ± 0%   -69.25%  (p=0.000 n=10+10)
Dictionary/Bounds/INT64                      774ns ± 0%   317ns ± 1%   -59.06%  (p=0.000 n=10+9)
Dictionary/Bounds/INT96                     6.52µs ± 0%  6.28µs ± 1%    -3.70%  (p=0.000 n=10+10)
Dictionary/Bounds/FLOAT                     1.16µs ± 0%  0.24µs ± 0%   -79.37%  (p=0.000 n=9+10)
Dictionary/Bounds/DOUBLE                    1.16µs ± 0%  0.31µs ± 0%   -73.02%  (p=0.000 n=10+9)
Dictionary/Bounds/BYTE_ARRAY                9.72µs ± 0%  7.77µs ± 0%   -20.02%  (p=0.000 n=10+9)
Dictionary/Bounds/FIXED_LEN_BYTE_ARRAY(16)  8.53µs ± 0%  7.50µs ± 1%   -12.04%  (p=0.000 n=10+10)
Dictionary/Bounds/INT(32,false)              892ns ± 0%   237ns ± 0%   -73.46%  (p=0.000 n=9+10)
Dictionary/Bounds/INT(64,false)              892ns ± 0%   318ns ± 0%   -64.37%  (p=0.000 n=10+10)
Dictionary/Lookup/BOOLEAN                   2.30µs ± 0%  1.75µs ± 0%   -24.19%  (p=0.000 n=9+9)
Dictionary/Lookup/INT32                     1.43µs ± 1%  0.62µs ± 2%   -56.56%  (p=0.000 n=9+9)
Dictionary/Lookup/INT64                     1.40µs ± 0%  0.64µs ± 0%   -54.30%  (p=0.000 n=10+8)
Dictionary/Lookup/INT96                     20.4µs ± 2%  19.8µs ± 2%    -2.74%  (p=0.001 n=10+9)
Dictionary/Lookup/FLOAT                     1.63µs ± 0%  0.63µs ± 1%   -61.47%  (p=0.000 n=8+9)
Dictionary/Lookup/DOUBLE                    1.41µs ± 0%  0.65µs ± 4%   -54.34%  (p=0.000 n=9+10)
Dictionary/Lookup/BYTE_ARRAY                3.46µs ± 0%  0.98µs ± 1%   -71.58%  (p=0.000 n=9+9)
Dictionary/Lookup/FIXED_LEN_BYTE_ARRAY(16)  2.52µs ± 0%  0.91µs ± 0%   -63.78%  (p=0.000 n=10+9)
Dictionary/Lookup/INT(32,false)             1.40µs ± 1%  0.61µs ± 1%   -56.47%  (p=0.000 n=10+9)
Dictionary/Lookup/INT(64,false)             1.42µs ± 2%  0.73µs ± 0%   -48.42%  (p=0.000 n=9+10)

name                                        old value/s  new value/s  delta
Dictionary/Bounds/BOOLEAN                    34.1G ± 0%   34.1G ± 0%      ~     (p=0.739 n=10+10)
Dictionary/Bounds/INT32                      1.29G ± 0%   4.20G ± 0%  +225.23%  (p=0.000 n=10+10)
Dictionary/Bounds/INT64                      1.29G ± 0%   3.16G ± 1%  +144.27%  (p=0.000 n=10+9)
Dictionary/Bounds/INT96                       153M ± 0%    159M ± 1%    +3.84%  (p=0.000 n=10+10)
Dictionary/Bounds/FLOAT                       863M ± 0%   4181M ± 0%  +384.74%  (p=0.000 n=10+10)
Dictionary/Bounds/DOUBLE                      865M ± 0%   3205M ± 0%  +270.63%  (p=0.000 n=10+9)
Dictionary/Bounds/BYTE_ARRAY                  103M ± 0%    129M ± 0%   +25.03%  (p=0.000 n=10+9)
Dictionary/Bounds/FIXED_LEN_BYTE_ARRAY(16)    117M ± 0%    133M ± 1%   +13.69%  (p=0.000 n=10+10)
Dictionary/Bounds/INT(32,false)              1.12G ± 0%   4.22G ± 0%  +276.72%  (p=0.000 n=8+10)
Dictionary/Bounds/INT(64,false)              1.12G ± 0%   3.15G ± 0%  +180.65%  (p=0.000 n=10+10)
Dictionary/Lookup/BOOLEAN                     434M ± 0%    573M ± 0%   +31.89%  (p=0.000 n=9+9)
Dictionary/Lookup/INT32                       701M ± 1%   1613M ± 2%  +130.18%  (p=0.000 n=9+9)
Dictionary/Lookup/INT64                       715M ± 0%   1565M ± 0%  +118.81%  (p=0.000 n=10+8)
Dictionary/Lookup/INT96                      49.1M ± 3%   50.5M ± 2%    +2.81%  (p=0.001 n=10+9)
Dictionary/Lookup/FLOAT                       615M ± 0%   1596M ± 1%  +159.54%  (p=0.000 n=8+9)
Dictionary/Lookup/DOUBLE                      707M ± 0%   1549M ± 4%  +119.09%  (p=0.000 n=9+10)
Dictionary/Lookup/BYTE_ARRAY                  289M ± 0%   1017M ± 1%  +251.86%  (p=0.000 n=9+9)
Dictionary/Lookup/FIXED_LEN_BYTE_ARRAY(16)    397M ± 0%   1096M ± 0%  +176.05%  (p=0.000 n=9+9)
Dictionary/Lookup/INT(32,false)               713M ± 1%   1639M ± 1%  +129.75%  (p=0.000 n=10+9)
Dictionary/Lookup/INT(64,false)               702M ± 2%   1361M ± 0%   +93.85%  (p=0.000 n=9+10)
```

Patch:
diff --git a/array.go b/array.go
new file mode 100644
index 0000000..b58028b
--- /dev/null
+++ b/array.go
@@ -0,0 +1,33 @@
+package parquet
+
+import "unsafe"
+
+type array struct {
+	ptr unsafe.Pointer
+	len int
+}
+
+func makeArrayString(values []string) array {
+	return *(*array)(unsafe.Pointer(&values))
+}
+
+func makeArrayValue(values []Value) array {
+	return *(*array)(unsafe.Pointer(&values))
+}
+
+func (a array) index(i int, size, offset uintptr) unsafe.Pointer {
+	return unsafe.Add(a.ptr, uintptr(i)*size+offset)
+}
+
+func (a array) slice(i, j int, size, offset uintptr) array {
+	if i < 0 || i > a.len || j < 0 || j > a.len {
+		panic("slice index out of bounds")
+	}
+	if i > j {
+		panic("negative slice length")
+	}
+	return array{
+		ptr: a.index(i, size, offset),
+		len: j - i,
+	}
+}
diff --git a/array_go18.go b/array_go18.go
new file mode 100644
index 0000000..841f3ed
--- /dev/null
+++ b/array_go18.go
@@ -0,0 +1,20 @@
+//go:build go1.18
+
+package parquet
+
+import "unsafe"
+
+func makeArray[T any](s []T) array {
+	return array{
+		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&s)),
+		len: len(s),
+	}
+}
+
+func makeSlice[T any](a array) []T {
+	return slice[T](a.ptr, a.len)
+}
+
+func slice[T any](p unsafe.Pointer, n int) []T {
+	return unsafe.Slice((*T)(p), n)
+}
diff --git a/column_buffer.go b/column_buffer.go
index 4311b04..3fed331 100644
--- a/column_buffer.go
+++ b/column_buffer.go
@@ -79,32 +79,6 @@ type ColumnBuffer interface {
 	writeValues(rows array, size, offset uintptr, levels columnLevels)
 }
 
-type array struct {
-	ptr unsafe.Pointer
-	len int
-}
-
-func makeValueArray(values []Value) array {
-	return *(*array)(unsafe.Pointer(&values))
-}
-
-func (a array) index(i int, size, offset uintptr) unsafe.Pointer {
-	return unsafe.Add(a.ptr, uintptr(i)*size+offset)
-}
-
-func (a array) slice(i, j int, size, offset uintptr) array {
-	if i < 0 || i > a.len || j < 0 || j > a.len {
-		panic("slice index out of bounds")
-	}
-	if i > j {
-		panic("negative slice length")
-	}
-	return array{
-		ptr: a.index(i, size, offset),
-		len: j - i,
-	}
-}
-
 type columnLevels struct {
 	repetitionDepth byte
 	repetitionLevel byte
@@ -841,7 +815,7 @@ func (col *booleanColumnBuffer) WriteBooleans(values []bool) (int, error) {
 
 func (col *booleanColumnBuffer) WriteValues(values []Value) (int, error) {
 	var value Value
-	col.writeValues(makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
+	col.writeValues(makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
 	return len(values), nil
 }
 
@@ -1005,7 +979,7 @@ func (col *int32ColumnBuffer) WriteInt32s(values []int32) (int, error) {
 
 func (col *int32ColumnBuffer) WriteValues(values []Value) (int, error) {
 	var value Value
-	col.writeValues(makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
+	col.writeValues(makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
 	return len(values), nil
 }
 
@@ -1108,7 +1082,7 @@ func (col *int64ColumnBuffer) WriteInt64s(values []int64) (int, error) {
 
 func (col *int64ColumnBuffer) WriteValues(values []Value) (int, error) {
 	var value Value
-	col.writeValues(makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
+	col.writeValues(makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
 	return len(values), nil
 }
 
@@ -1304,7 +1278,7 @@ func (col *floatColumnBuffer) WriteFloats(values []float32) (int, error) {
 
 func (col *floatColumnBuffer) WriteValues(values []Value) (int, error) {
 	var value Value
-	col.writeValues(makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
+	col.writeValues(makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
 	return len(values), nil
 }
 
@@ -1406,7 +1380,7 @@ func (col *doubleColumnBuffer) WriteDoubles(values []float64) (int, error) {
 
 func (col *doubleColumnBuffer) WriteValues(values []Value) (int, error) {
 	var value Value
-	col.writeValues(makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
+	col.writeValues(makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
 	return len(values), nil
 }
 
@@ -1559,7 +1533,7 @@ func (col *byteArrayColumnBuffer) writeByteArrays(values []byte) (count, bytes i
 
 func (col *byteArrayColumnBuffer) WriteValues(values []Value) (int, error) {
 	var value Value
-	col.writeValues(makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.ptr), columnLevels{})
+	col.writeValues(makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.ptr), columnLevels{})
 	return len(values), nil
 }
 
@@ -1791,7 +1765,7 @@ func (col *uint32ColumnBuffer) WriteUint32s(values []uint32) (int, error) {
 
 func (col *uint32ColumnBuffer) WriteValues(values []Value) (int, error) {
 	var value Value
-	col.writeValues(makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
+	col.writeValues(makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
 	return len(values), nil
 }
 
@@ -1893,7 +1867,7 @@ func (col *uint64ColumnBuffer) WriteUint64s(values []uint64) (int, error) {
 
 func (col *uint64ColumnBuffer) WriteValues(values []Value) (int, error) {
 	var value Value
-	col.writeValues(makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
+	col.writeValues(makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64), columnLevels{})
 	return len(values), nil
 }
 
diff --git a/column_buffer_amd64.s b/column_buffer_amd64.s
index d848fa4..4fa5812 100644
--- a/column_buffer_amd64.s
+++ b/column_buffer_amd64.s
@@ -3,19 +3,19 @@
 #include "textflag.h"
 
 // func broadcastRangeInt32AVX2(dst []int32, base int32)
-TEXT ·broadcastRangeInt32AVX2(SB), NOSPLIT, $0-32
-    MOVQ dst+0(FP), AX
-    MOVQ dst+8(FP), BX
+TEXT ·broadcastRangeInt32AVX2(SB), NOSPLIT, $0-28
+    MOVQ dst_base+0(FP), AX
+    MOVQ dst_len+8(FP), BX
     MOVL base+24(FP), CX
     XORQ SI, SI
 
     CMPQ BX, $8
     JB test1x4
 
-    VMOVDQU rangeInt32<>(SB), Y0         // [0,1,2,3,4,5,6,7]
-    VPBROADCASTD rangeInt32<>+32(SB), Y1 // [8,8,8,8,8,8,8,8]
-    VPBROADCASTD base+24(FP), Y2         // [base...]
-    VPADDD Y2, Y0, Y0                    // [base,base+1,...]
+    VMOVDQU ·range0n8(SB), Y0         // [0,1,2,3,4,5,6,7]
+    VPBROADCASTD ·range0n8+32(SB), Y1 // [8,8,8,8,8,8,8,8]
+    VPBROADCASTD base+24(FP), Y2      // [base...]
+    VPADDD Y2, Y0, Y0                 // [base,base+1,...]
 
     MOVQ BX, DI
     SHRQ $3, DI
@@ -41,21 +41,10 @@ test1x4:
     JNE loop1x4
     RET
 
-GLOBL rangeInt32<>(SB), RODATA|NOPTR, $40
-DATA rangeInt32<>+0(SB)/4, $0
-DATA rangeInt32<>+4(SB)/4, $1
-DATA rangeInt32<>+8(SB)/4, $2
-DATA rangeInt32<>+12(SB)/4, $3
-DATA rangeInt32<>+16(SB)/4, $4
-DATA rangeInt32<>+20(SB)/4, $5
-DATA rangeInt32<>+24(SB)/4, $6
-DATA rangeInt32<>+28(SB)/4, $7
-DATA rangeInt32<>+32(SB)/4, $8
-
 // func writeValuesBitpackAVX2(values unsafe.Pointer, rows array, size, offset uintptr)
 TEXT ·writeValuesBitpackAVX2(SB), NOSPLIT, $0-40
-    MOVQ values_base+0(FP), AX
-    MOVQ rows_base+8(FP), BX
+    MOVQ values+0(FP), AX
+    MOVQ rows_ptr+8(FP), BX
     MOVQ rows_len+16(FP), CX
     MOVQ size+24(FP), DX
     MOVQ offset+32(FP), DI
@@ -79,7 +68,7 @@ init:
     JB loop
 
     VPBROADCASTD size+24(FP), Y0
-    VPMULLD ·scale8x4(SB), Y0, Y0
+    VPMULLD ·range0n8(SB), Y0, Y0
     VPCMPEQD Y1, Y1, Y1
     VPCMPEQD Y2, Y2, Y2
     VPCMPEQD Y3, Y3, Y3
@@ -147,8 +136,8 @@ loop:
 
 // func writeValues32bitsAVX2(values unsafe.Pointer, rows array, size, offset uintptr)
 TEXT ·writeValues32bitsAVX2(SB), NOSPLIT, $0-40
-    MOVQ values_base+0(FP), AX
-    MOVQ rows_base+8(FP), BX
+    MOVQ values+0(FP), AX
+    MOVQ rows_ptr+8(FP), BX
     MOVQ rows_len+16(FP), CX
     MOVQ size+24(FP), DX
 
@@ -166,7 +155,7 @@ TEXT ·writeValues32bitsAVX2(SB), NOSPLIT, $0-40
     SHLQ $3, DI
 
     VPBROADCASTD size+24(FP), Y0
-    VPMULLD ·scale8x4(SB), Y0, Y0
+    VPMULLD ·range0n8(SB), Y0, Y0
     VPCMPEQD Y1, Y1, Y1
     VPCMPEQD Y2, Y2, Y2
 loop8x4:
@@ -196,8 +185,8 @@ done:
 
 // func writeValues64bitsAVX2(values unsafe.Pointer, rows array, size, offset uintptr)
 TEXT ·writeValues64bitsAVX2(SB), NOSPLIT, $0-40
-    MOVQ values_base+0(FP), AX
-    MOVQ rows_base+8(FP), BX
+    MOVQ values+0(FP), AX
+    MOVQ rows_ptr+8(FP), BX
     MOVQ rows_len+16(FP), CX
     MOVQ size+24(FP), DX
 
@@ -244,8 +233,8 @@ done:
 
 // func writeValues128bitsAVX2(values unsafe.Pointer, rows array, size, offset uintptr)
 TEXT ·writeValues128bitsAVX2(SB), NOSPLIT, $0-40
-    MOVQ values_base+0(FP), AX
-    MOVQ rows_base+8(FP), BX
+    MOVQ values+0(FP), AX
+    MOVQ rows_ptr+8(FP), BX
     MOVQ rows_len+16(FP), CX
     MOVQ size+24(FP), DX
 
@@ -289,16 +278,6 @@ loop1x16:
 done:
     RET
 
-GLOBL ·scale8x4(SB), RODATA|NOPTR, $32
-DATA ·scale8x4+0(SB)/4,  $0
-DATA ·scale8x4+4(SB)/4,  $1
-DATA ·scale8x4+8(SB)/4,  $2
-DATA ·scale8x4+12(SB)/4, $3
-DATA ·scale8x4+16(SB)/4, $4
-DATA ·scale8x4+20(SB)/4, $5
-DATA ·scale8x4+24(SB)/4, $6
-DATA ·scale8x4+28(SB)/4, $7
-
 GLOBL ·scale4x8(SB), RODATA|NOPTR, $32
 DATA ·scale4x8+0(SB)/8,  $0
 DATA ·scale4x8+8(SB)/8,  $1
diff --git a/column_buffer_go18.go b/column_buffer_go18.go
index c864992..8f01d8f 100644
--- a/column_buffer_go18.go
+++ b/column_buffer_go18.go
@@ -10,21 +10,6 @@ import (
 	"github.com/segmentio/parquet-go/deprecated"
 )
 
-func makeArray[T any](s []T) array {
-	return array{
-		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&s)),
-		len: len(s),
-	}
-}
-
-func makeSlice[T any](a array) []T {
-	return slice[T](a.ptr, a.len)
-}
-
-func slice[T any](p unsafe.Pointer, n int) []T {
-	return unsafe.Slice((*T)(p), n)
-}
-
 // writeRowsFunc is the type of functions that apply rows to a set of column
 // buffers.
 //
diff --git a/dictionary.go b/dictionary.go
index ab6d0fc..ac2d5ed 100644
--- a/dictionary.go
+++ b/dictionary.go
@@ -1,8 +1,8 @@
 package parquet
 
 import (
-	"bytes"
 	"io"
+	. "math/bits"
 	"unsafe"
 
 	"github.com/segmentio/parquet-go/deprecated"
@@ -53,7 +53,7 @@ type Dictionary interface {
 	Lookup(indexes []int32, values []Value)
 
 	// Returns the min and max values found in the given indexes.
-	Bounds(indexed []int32) (min, max Value)
+	Bounds(indexes []int32) (min, max Value)
 
 	// Resets the dictionary to its initial state, removing all values.
 	Reset()
@@ -67,15 +67,40 @@ type Dictionary interface {
 	// See ColumnBuffer.writeValues for details on the use of unexported methods
 	// on interfaces.
 	insert(indexes []int32, rows array, size, offset uintptr)
+	//lookup(indexes []int32, rows array, size, offset uintptr)
+}
+
+func checkLookupIndexBounds(indexes []int32, rows array) {
+	if rows.len < len(indexes) {
+		panic("dictionary lookup with more indexes than values")
+	}
 }
 
 // The boolean dictionary always contains two values for true and false.
 type booleanDictionary struct {
 	booleanPage
-	index map[bool]int32
+	// There are only two possible values for booleans, false and true.
+	// Rather than using a Go map, we track the indexes of each values
+	// in an array of two 32 bits integers. When inserting values in the
+	// dictionary, we ensure that an index exist for each boolean value,
+	// then use the value 0 or 1 (false or true) to perform a lookup in
+	// the dictionary's map.
+	hashmap [2]int32
 }
 
 func newBooleanDictionary(typ Type, columnIndex int16, numValues int32, values []byte) *booleanDictionary {
+	indexOfFalse, indexOfTrue := int32(-1), int32(-1)
+
+	for i := int32(0); i < numValues && indexOfFalse < 0 && indexOfTrue < 0; i += 8 {
+		v := values[i]
+		if v != 0x00 {
+			indexOfTrue = i + int32(TrailingZeros8(v))
+		}
+		if v != 0xFF {
+			indexOfFalse = i + int32(TrailingZeros8(^v))
+		}
+	}
+
 	return &booleanDictionary{
 		booleanPage: booleanPage{
 			typ:         typ,
@@ -83,6 +108,10 @@ func newBooleanDictionary(typ Type, columnIndex int16, numValues int32, values [
 			numValues:   numValues,
 			columnIndex: ^columnIndex,
 		},
+		hashmap: [2]int32{
+			0: indexOfFalse,
+			1: indexOfTrue,
+		},
 	}
 }
 
@@ -90,41 +119,48 @@ func (d *booleanDictionary) Type() Type { return newIndexedType(d.typ, d) }
 
 func (d *booleanDictionary) Len() int { return int(d.numValues) }
 
-func (d *booleanDictionary) Index(i int32) Value { return makeValueBoolean(d.valueAt(int(i))) }
+func (d *booleanDictionary) Index(i int32) Value { return d.makeValue(d.index(i)) }
+
+func (d *booleanDictionary) index(i int32) bool { return d.valueAt(int(i)) }
 
 func (d *booleanDictionary) Insert(indexes []int32, values []Value) {
 	var value Value
-	d.insert(indexes, makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
+	d.insert(indexes, makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 }
 
 func (d *booleanDictionary) insert(indexes []int32, rows array, size, offset uintptr) {
 	_ = indexes[:rows.len]
 
-	if d.index == nil {
-		d.index = make(map[bool]int32, cap(d.bits))
-		for i := 0; i < int(d.numValues); i++ {
-			d.index[d.valueAt(i)] = int32(i)
-		}
+	if d.hashmap[0] < 0 {
+		d.hashmap[0] = d.numValues
+		d.numValues++
+		d.bits = plain.AppendBoolean(d.bits, int(d.hashmap[0]), false)
 	}
 
-	for i := 0; i < rows.len; i++ {
-		value := *(*bool)(rows.index(i, size, offset))
+	if d.hashmap[1] < 0 {
+		d.hashmap[1] = d.numValues
+		d.numValues++
+		d.bits = plain.AppendBoolean(d.bits, int(d.hashmap[1]), true)
+	}
 
-		index, exists := d.index[value]
-		if !exists {
-			index = d.numValues
-			d.bits = plain.AppendBoolean(d.bits, int(index), value)
-			d.index[value] = index
-			d.numValues++
-		}
+	dict := d.hashmap
 
-		indexes[i] = index
+	for i := 0; i < rows.len; i++ {
+		v := *(*byte)(rows.index(i, size, offset)) & 1
+		indexes[i] = dict[v]
 	}
 }
 
 func (d *booleanDictionary) Lookup(indexes []int32, values []Value) {
+	model := d.makeValue(false)
+	memsetValues(values, model)
+	d.lookup(indexes, makeArrayValue(values), unsafe.Sizeof(model), unsafe.Offsetof(model.u64))
+}
+
+func (d *booleanDictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
 	for i, j := range indexes {
-		values[i] = d.Index(j)
+		*(*bool)(rows.index(i, size, offset)) = d.index(j)
 	}
 }
 
@@ -133,7 +169,7 @@ func (d *booleanDictionary) Bounds(indexes []int32) (min, max Value) {
 		hasFalse, hasTrue := false, false
 
 		for _, i := range indexes {
-			v := d.valueAt(int(i))
+			v := d.index(i)
 			if v {
 				hasTrue = true
 			} else {
@@ -144,8 +180,8 @@ func (d *booleanDictionary) Bounds(indexes []int32) (min, max Value) {
 			}
 		}
 
-		min = makeValueBoolean(!hasFalse)
-		max = makeValueBoolean(hasTrue)
+		min = d.makeValue(!hasFalse)
+		max = d.makeValue(hasTrue)
 	}
 	return min, max
 }
@@ -154,7 +190,7 @@ func (d *booleanDictionary) Reset() {
 	d.bits = d.bits[:0]
 	d.offset = 0
 	d.numValues = 0
-	d.index = nil
+	d.hashmap = [2]int32{-1, -1}
 }
 
 func (d *booleanDictionary) Page() BufferedPage {
@@ -163,7 +199,7 @@ func (d *booleanDictionary) Page() BufferedPage {
 
 type int32Dictionary struct {
 	int32Page
-	index map[int32]int32
+	hashmap map[int32]int32
 }
 
 func newInt32Dictionary(typ Type, columnIndex int16, numValues int32, values []byte) *int32Dictionary {
@@ -180,31 +216,33 @@ func (d *int32Dictionary) Type() Type { return newIndexedType(d.typ, d) }
 
 func (d *int32Dictionary) Len() int { return len(d.values) }
 
-func (d *int32Dictionary) Index(i int32) Value { return makeValueInt32(d.values[i]) }
+func (d *int32Dictionary) Index(i int32) Value { return d.makeValue(d.index(i)) }
+
+func (d *int32Dictionary) index(i int32) int32 { return d.values[i] }
 
 func (d *int32Dictionary) Insert(indexes []int32, values []Value) {
 	var value Value
-	d.insert(indexes, makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
+	d.insert(indexes, makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 }
 
 func (d *int32Dictionary) insert(indexes []int32, rows array, size, offset uintptr) {
 	_ = indexes[:rows.len]
 
-	if d.index == nil {
-		d.index = make(map[int32]int32, cap(d.values))
+	if d.hashmap == nil {
+		d.hashmap = make(map[int32]int32, cap(d.values))
 		for i, v := range d.values {
-			d.index[v] = int32(i)
+			d.hashmap[v] = int32(i)
 		}
 	}
 
 	for i := 0; i < rows.len; i++ {
 		value := *(*int32)(rows.index(i, size, offset))
 
-		index, exists := d.index[value]
+		index, exists := d.hashmap[value]
 		if !exists {
 			index = int32(len(d.values))
 			d.values = append(d.values, value)
-			d.index[value] = index
+			d.hashmap[value] = index
 		}
 
 		indexes[i] = index
@@ -212,35 +250,23 @@ func (d *int32Dictionary) insert(indexes []int32, rows array, size, offset uintp
 }
 
 func (d *int32Dictionary) Lookup(indexes []int32, values []Value) {
-	for i, j := range indexes {
-		values[i] = d.Index(j)
-	}
+	model := d.makeValue(0)
+	memsetValues(values, model)
+	d.lookup(indexes, makeArrayValue(values), unsafe.Sizeof(model), unsafe.Offsetof(model.u64))
 }
 
 func (d *int32Dictionary) Bounds(indexes []int32) (min, max Value) {
 	if len(indexes) > 0 {
-		minValue := d.values[indexes[0]]
-		maxValue := minValue
-
-		for _, i := range indexes[1:] {
-			value := d.values[i]
-			switch {
-			case value < minValue:
-				minValue = value
-			case value > maxValue:
-				maxValue = value
-			}
-		}
-
-		min = makeValueInt32(minValue)
-		max = makeValueInt32(maxValue)
+		minValue, maxValue := d.bounds(indexes)
+		min = d.makeValue(minValue)
+		max = d.makeValue(maxValue)
 	}
 	return min, max
 }
 
 func (d *int32Dictionary) Reset() {
 	d.values = d.values[:0]
-	d.index = nil
+	d.hashmap = nil
 }
 
 func (d *int32Dictionary) Page() BufferedPage {
@@ -249,7 +275,7 @@ func (d *int32Dictionary) Page() BufferedPage {
 
 type int64Dictionary struct {
 	int64Page
-	index map[int64]int32
+	hashmap map[int64]int32
 }
 
 func newInt64Dictionary(typ Type, columnIndex int16, numValues int32, values []byte) *int64Dictionary {
@@ -266,31 +292,33 @@ func (d *int64Dictionary) Type() Type { return newIndexedType(d.typ, d) }
 
 func (d *int64Dictionary) Len() int { return len(d.values) }
 
-func (d *int64Dictionary) Index(i int32) Value { return makeValueInt64(d.values[i]) }
+func (d *int64Dictionary) Index(i int32) Value { return d.makeValue(d.index(i)) }
+
+func (d *int64Dictionary) index(i int32) int64 { return d.values[i] }
 
 func (d *int64Dictionary) Insert(indexes []int32, values []Value) {
 	var value Value
-	d.insert(indexes, makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
+	d.insert(indexes, makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 }
 
 func (d *int64Dictionary) insert(indexes []int32, rows array, size, offset uintptr) {
 	_ = indexes[:rows.len]
 
-	if d.index == nil {
-		d.index = make(map[int64]int32, cap(d.values))
+	if d.hashmap == nil {
+		d.hashmap = make(map[int64]int32, cap(d.values))
 		for i, v := range d.values {
-			d.index[v] = int32(i)
+			d.hashmap[v] = int32(i)
 		}
 	}
 
 	for i := 0; i < rows.len; i++ {
 		value := *(*int64)(rows.index(i, size, offset))
 
-		index, exists := d.index[value]
+		index, exists := d.hashmap[value]
 		if !exists {
 			index = int32(len(d.values))
 			d.values = append(d.values, value)
-			d.index[value] = index
+			d.hashmap[value] = index
 		}
 
 		indexes[i] = index
@@ -298,35 +326,23 @@ func (d *int64Dictionary) insert(indexes []int32, rows array, size, offset uintp
 }
 
 func (d *int64Dictionary) Lookup(indexes []int32, values []Value) {
-	for i, j := range indexes {
-		values[i] = d.Index(j)
-	}
+	model := d.makeValue(0)
+	memsetValues(values, model)
+	d.lookup(indexes, makeArrayValue(values), unsafe.Sizeof(model), unsafe.Offsetof(model.u64))
 }
 
 func (d *int64Dictionary) Bounds(indexes []int32) (min, max Value) {
 	if len(indexes) > 0 {
-		minValue := d.values[indexes[0]]
-		maxValue := minValue
-
-		for _, i := range indexes[1:] {
-			value := d.values[i]
-			switch {
-			case value < minValue:
-				minValue = value
-			case value > maxValue:
-				maxValue = value
-			}
-		}
-
-		min = makeValueInt64(minValue)
-		max = makeValueInt64(maxValue)
+		minValue, maxValue := d.bounds(indexes)
+		min = d.makeValue(minValue)
+		max = d.makeValue(maxValue)
 	}
 	return min, max
 }
 
 func (d *int64Dictionary) Reset() {
 	d.values = d.values[:0]
-	d.index = nil
+	d.hashmap = nil
 }
 
 func (d *int64Dictionary) Page() BufferedPage {
@@ -335,7 +351,7 @@ func (d *int64Dictionary) Page() BufferedPage {
 
 type int96Dictionary struct {
 	int96Page
-	index map[deprecated.Int96]int32
+	hashmap map[deprecated.Int96]int32
 }
 
 func newInt96Dictionary(typ Type, columnIndex int16, numValues int32, values []byte) *int96Dictionary {
@@ -352,7 +368,9 @@ func (d *int96Dictionary) Type() Type { return newIndexedType(d.typ, d) }
 
 func (d *int96Dictionary) Len() int { return len(d.values) }
 
-func (d *int96Dictionary) Index(i int32) Value { return makeValueInt96(d.values[i]) }
+func (d *int96Dictionary) Index(i int32) Value { return d.makeValue(d.index(i)) }
+
+func (d *int96Dictionary) index(i int32) deprecated.Int96 { return d.values[i] }
 
 func (d *int96Dictionary) Insert(indexes []int32, values []Value) {
 	d.insertValues(indexes, len(values), func(i int) deprecated.Int96 {
@@ -369,21 +387,21 @@ func (d *int96Dictionary) insert(indexes []int32, rows array, size, offset uintp
 func (d *int96Dictionary) insertValues(indexes []int32, count int, valueAt func(int) deprecated.Int96) {
 	_ = indexes[:count]
 
-	if d.index == nil {
-		d.index = make(map[deprecated.Int96]int32, cap(d.values))
+	if d.hashmap == nil {
+		d.hashmap = make(map[deprecated.Int96]int32, cap(d.values))
 		for i, v := range d.values {
-			d.index[v] = int32(i)
+			d.hashmap[v] = int32(i)
 		}
 	}
 
 	for i := 0; i < count; i++ {
 		value := valueAt(i)
 
-		index, exists := d.index[value]
+		index, exists := d.hashmap[value]
 		if !exists {
 			index = int32(len(d.values))
 			d.values = append(d.values, value)
-			d.index[value] = index
+			d.hashmap[value] = index
 		}
 
 		indexes[i] = index
@@ -398,11 +416,11 @@ func (d *int96Dictionary) Lookup(indexes []int32, values []Value) {
 
 func (d *int96Dictionary) Bounds(indexes []int32) (min, max Value) {
 	if len(indexes) > 0 {
-		minValue := d.values[indexes[0]]
+		minValue := d.index(indexes[0])
 		maxValue := minValue
 
 		for _, i := range indexes[1:] {
-			value := d.values[i]
+			value := d.index(i)
 			switch {
 			case value.Less(minValue):
 				minValue = value
@@ -411,15 +429,15 @@ func (d *int96Dictionary) Bounds(indexes []int32) (min, max Value) {
 			}
 		}
 
-		min = makeValueInt96(minValue)
-		max = makeValueInt96(maxValue)
+		min = d.makeValue(minValue)
+		max = d.makeValue(maxValue)
 	}
 	return min, max
 }
 
 func (d *int96Dictionary) Reset() {
 	d.values = d.values[:0]
-	d.index = nil
+	d.hashmap = nil
 }
 
 func (d *int96Dictionary) Page() BufferedPage {
@@ -428,7 +446,7 @@ func (d *int96Dictionary) Page() BufferedPage {
 
 type floatDictionary struct {
 	floatPage
-	index map[float32]int32
+	hashmap map[float32]int32
 }
 
 func newFloatDictionary(typ Type, columnIndex int16, numValues int32, values []byte) *floatDictionary {
@@ -445,31 +463,33 @@ func (d *floatDictionary) Type() Type { return newIndexedType(d.typ, d) }
 
 func (d *floatDictionary) Len() int { return len(d.values) }
 
-func (d *floatDictionary) Index(i int32) Value { return makeValueFloat(d.values[i]) }
+func (d *floatDictionary) Index(i int32) Value { return d.makeValue(d.index(i)) }
+
+func (d *floatDictionary) index(i int32) float32 { return d.values[i] }
 
 func (d *floatDictionary) Insert(indexes []int32, values []Value) {
 	var value Value
-	d.insert(indexes, makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
+	d.insert(indexes, makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 }
 
 func (d *floatDictionary) insert(indexes []int32, rows array, size, offset uintptr) {
 	_ = indexes[:rows.len]
 
-	if d.index == nil {
-		d.index = make(map[float32]int32, cap(d.values))
+	if d.hashmap == nil {
+		d.hashmap = make(map[float32]int32, cap(d.values))
 		for i, v := range d.values {
-			d.index[v] = int32(i)
+			d.hashmap[v] = int32(i)
 		}
 	}
 
 	for i := 0; i < rows.len; i++ {
 		value := *(*float32)(rows.index(i, size, offset))
 
-		index, exists := d.index[value]
+		index, exists := d.hashmap[value]
 		if !exists {
 			index = int32(len(d.values))
 			d.values = append(d.values, value)
-			d.index[value] = index
+			d.hashmap[value] = index
 		}
 
 		indexes[i] = index
@@ -477,35 +497,23 @@ func (d *floatDictionary) insert(indexes []int32, rows array, size, offset uintp
 }
 
 func (d *floatDictionary) Lookup(indexes []int32, values []Value) {
-	for i, j := range indexes {
-		values[i] = d.Index(j)
-	}
+	model := d.makeValue(0)
+	memsetValues(values, model)
+	d.lookup(indexes, makeArrayValue(values), unsafe.Sizeof(model), unsafe.Offsetof(model.u64))
 }
 
 func (d *floatDictionary) Bounds(indexes []int32) (min, max Value) {
 	if len(indexes) > 0 {
-		minValue := d.values[indexes[0]]
-		maxValue := minValue
-
-		for _, i := range indexes[1:] {
-			value := d.values[i]
-			switch {
-			case value < minValue:
-				minValue = value
-			case value > maxValue:
-				maxValue = value
-			}
-		}
-
-		min = makeValueFloat(minValue)
-		max = makeValueFloat(maxValue)
+		minValue, maxValue := d.bounds(indexes)
+		min = d.makeValue(minValue)
+		max = d.makeValue(maxValue)
 	}
 	return min, max
 }
 
 func (d *floatDictionary) Reset() {
 	d.values = d.values[:0]
-	d.index = nil
+	d.hashmap = nil
 }
 
 func (d *floatDictionary) Page() BufferedPage {
@@ -514,7 +522,7 @@ func (d *floatDictionary) Page() BufferedPage {
 
 type doubleDictionary struct {
 	doublePage
-	index map[float64]int32
+	hashmap map[float64]int32
 }
 
 func newDoubleDictionary(typ Type, columnIndex int16, numValues int32, values []byte) *doubleDictionary {
@@ -531,31 +539,33 @@ func (d *doubleDictionary) Type() Type { return newIndexedType(d.typ, d) }
 
 func (d *doubleDictionary) Len() int { return len(d.values) }
 
-func (d *doubleDictionary) Index(i int32) Value { return makeValueDouble(d.values[i]) }
+func (d *doubleDictionary) Index(i int32) Value { return d.makeValue(d.index(i)) }
+
+func (d *doubleDictionary) index(i int32) float64 { return d.values[i] }
 
 func (d *doubleDictionary) Insert(indexes []int32, values []Value) {
 	var value Value
-	d.insert(indexes, makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
+	d.insert(indexes, makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 }
 
 func (d *doubleDictionary) insert(indexes []int32, rows array, size, offset uintptr) {
 	_ = indexes[:rows.len]
 
-	if d.index == nil {
-		d.index = make(map[float64]int32, cap(d.values))
+	if d.hashmap == nil {
+		d.hashmap = make(map[float64]int32, cap(d.values))
 		for i, v := range d.values {
-			d.index[v] = int32(i)
+			d.hashmap[v] = int32(i)
 		}
 	}
 
 	for i := 0; i < rows.len; i++ {
 		value := *(*float64)(rows.index(i, size, offset))
 
-		index, exists := d.index[value]
+		index, exists := d.hashmap[value]
 		if !exists {
 			index = int32(len(d.values))
 			d.values = append(d.values, value)
-			d.index[value] = index
+			d.hashmap[value] = index
 		}
 
 		indexes[i] = index
@@ -563,35 +573,23 @@ func (d *doubleDictionary) insert(indexes []int32, rows array, size, offset uint
 }
 
 func (d *doubleDictionary) Lookup(indexes []int32, values []Value) {
-	for i, j := range indexes {
-		values[i] = d.Index(j)
-	}
+	model := d.makeValue(0)
+	memsetValues(values, model)
+	d.lookup(indexes, makeArrayValue(values), unsafe.Sizeof(model), unsafe.Offsetof(model.u64))
 }
 
 func (d *doubleDictionary) Bounds(indexes []int32) (min, max Value) {
 	if len(indexes) > 0 {
-		minValue := d.values[indexes[0]]
-		maxValue := minValue
-
-		for _, i := range indexes[1:] {
-			value := d.values[i]
-			switch {
-			case value < minValue:
-				minValue = value
-			case value > maxValue:
-				maxValue = value
-			}
-		}
-
-		min = makeValueDouble(minValue)
-		max = makeValueDouble(maxValue)
+		minValue, maxValue := d.bounds(indexes)
+		min = d.makeValue(minValue)
+		max = d.makeValue(maxValue)
 	}
 	return min, max
 }
 
 func (d *doubleDictionary) Reset() {
 	d.values = d.values[:0]
-	d.index = nil
+	d.hashmap = nil
 }
 
 func (d *doubleDictionary) Page() BufferedPage {
@@ -601,7 +599,7 @@ func (d *doubleDictionary) Page() BufferedPage {
 type byteArrayDictionary struct {
 	byteArrayPage
 	offsets []uint32
-	index   map[string]int32
+	hashmap map[string]int32
 }
 
 func newByteArrayDictionary(typ Type, columnIndex int16, numValues int32, values []byte) *byteArrayDictionary {
@@ -629,33 +627,33 @@ func (d *byteArrayDictionary) Type() Type { return newIndexedType(d.typ, d) }
 
 func (d *byteArrayDictionary) Len() int { return len(d.offsets) }
 
-func (d *byteArrayDictionary) Index(i int32) Value {
-	return makeValueBytes(ByteArray, d.valueAt(d.offsets[i]))
-}
+func (d *byteArrayDictionary) Index(i int32) Value { return d.makeValueBytes(d.index(i)) }
+
+func (d *byteArrayDictionary) index(i int32) []byte { return d.valueAt(d.offsets[i]) }
 
 func (d *byteArrayDictionary) Insert(indexes []int32, values []Value) {
 	var value Value
-	d.insert(indexes, makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.ptr))
+	d.insert(indexes, makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.ptr))
 }
 
 func (d *byteArrayDictionary) insert(indexes []int32, rows array, size, offset uintptr) {
 	_ = indexes[:rows.len]
 
-	if d.index == nil {
-		d.index = make(map[string]int32, cap(d.offsets))
+	if d.hashmap == nil {
+		d.hashmap = make(map[string]int32, cap(d.offsets))
 		for index, offset := range d.offsets {
-			d.index[bits.BytesToString(d.valueAt(offset))] = int32(index)
+			d.hashmap[string(d.valueAt(offset))] = int32(index)
 		}
 	}
 
 	for i := 0; i < rows.len; i++ {
 		value := *(*string)(rows.index(i, size, offset))
 
-		index, exists := d.index[value]
+		index, exists := d.hashmap[value]
 		if !exists {
 			index = int32(len(d.offsets))
 			value = d.append(value)
-			d.index[value] = index
+			d.hashmap[value] = index
 		}
 
 		indexes[i] = index
@@ -667,32 +665,42 @@ func (d *byteArrayDictionary) append(value string) string {
 	d.values = plain.AppendByteArrayString(d.values, value)
 	d.offsets = append(d.offsets, uint32(offset))
 	d.numValues++
-	return bits.BytesToString(d.values[offset+plain.ByteArrayLengthSize : len(d.values)])
+	return string(d.values[offset+plain.ByteArrayLengthSize : len(d.values)])
 }
 
 func (d *byteArrayDictionary) Lookup(indexes []int32, values []Value) {
-	for i, j := range indexes {
-		values[i] = d.Index(j)
-	}
+	model := d.makeValueString("")
+	memsetValues(values, model)
+	d.lookupString(indexes, makeArrayValue(values), unsafe.Sizeof(model), unsafe.Offsetof(model.ptr))
 }
 
 func (d *byteArrayDictionary) Bounds(indexes []int32) (min, max Value) {
 	if len(indexes) > 0 {
-		minValue := d.valueAt(d.offsets[indexes[0]])
+		base := d.index(indexes[0])
+		minValue := *(*string)(unsafe.Pointer(&base))
 		maxValue := minValue
+		values := [64]string{}
 
-		for _, i := range indexes[1:] {
-			value := d.valueAt(d.offsets[i])
-			switch {
-			case bytes.Compare(value, minValue) < 0:
-				minValue = value
-			case bytes.Compare(value, maxValue) > 0:
-				maxValue = value
+		for i := 1; i < len(indexes); i += len(values) {
+			n := len(indexes) - i
+			if n > len(values) {
+				n = len(values)
+			}
+			j := i + n
+			d.lookupString(indexes[i:j:j], makeArrayString(values[:n:n]), unsafe.Sizeof(values[0]), 0)
+
+			for _, value := range values[:n:n] {
+				switch {
+				case value < minValue:
+					minValue = value
+				case value > maxValue:
+					maxValue = value
+				}
 			}
 		}
 
-		min = makeValueBytes(ByteArray, minValue)
-		max = makeValueBytes(ByteArray, maxValue)
+		min = d.makeValueString(minValue)
+		max = d.makeValueString(maxValue)
 	}
 	return min, max
 }
@@ -701,7 +709,7 @@ func (d *byteArrayDictionary) Reset() {
 	d.offsets = d.offsets[:0]
 	d.values = d.values[:0]
 	d.numValues = 0
-	d.index = nil
+	d.hashmap = nil
 }
 
 func (d *byteArrayDictionary) Page() BufferedPage {
@@ -710,7 +718,7 @@ func (d *byteArrayDictionary) Page() BufferedPage {
 
 type fixedLenByteArrayDictionary struct {
 	fixedLenByteArrayPage
-	index map[string]int32
+	hashmap map[string]int32
 }
 
 func newFixedLenByteArrayDictionary(typ Type, columnIndex int16, numValues int32, data []byte) *fixedLenByteArrayDictionary {
@@ -730,11 +738,13 @@ func (d *fixedLenByteArrayDictionary) Type() Type { return newIndexedType(d.typ,
 func (d *fixedLenByteArrayDictionary) Len() int { return len(d.data) / d.size }
 
 func (d *fixedLenByteArrayDictionary) Index(i int32) Value {
-	return makeValueBytes(FixedLenByteArray, d.value(i))
+	return d.makeValueBytes(d.index(i))
 }
 
-func (d *fixedLenByteArrayDictionary) value(i int32) []byte {
-	return d.data[int(i)*d.size : int(i+1)*d.size]
+func (d *fixedLenByteArrayDictionary) index(i int32) []byte {
+	j := (int(i) + 0) * d.size
+	k := (int(i) + 1) * d.size
+	return d.data[j:k:k]
 }
 
 func (d *fixedLenByteArrayDictionary) Insert(indexes []int32, values []Value) {
@@ -752,10 +762,10 @@ func (d *fixedLenByteArrayDictionary) insert(indexes []int32, rows array, size,
 func (d *fixedLenByteArrayDictionary) insertValues(indexes []int32, count int, valueAt func(int) *byte) {
 	_ = indexes[:count]
 
-	if d.index == nil {
-		d.index = make(map[string]int32, cap(d.data)/d.size)
+	if d.hashmap == nil {
+		d.hashmap = make(map[string]int32, cap(d.data)/d.size)
 		for i, j := 0, int32(0); i < len(d.data); i += d.size {
-			d.index[bits.BytesToString(d.data[i:i+d.size])] = j
+			d.hashmap[string(d.data[i:i+d.size])] = j
 			j++
 		}
 	}
@@ -763,12 +773,12 @@ func (d *fixedLenByteArrayDictionary) insertValues(indexes []int32, count int, v
 	for i := 0; i < count; i++ {
 		value := unsafe.Slice(valueAt(i), d.size)
 
-		index, exists := d.index[string(value)]
+		index, exists := d.hashmap[string(value)]
 		if !exists {
 			index = int32(d.Len())
 			start := len(d.data)
 			d.data = append(d.data, value...)
-			d.index[bits.BytesToString(d.data[start:])] = index
+			d.hashmap[string(d.data[start:])] = index
 		}
 
 		indexes[i] = index
@@ -776,35 +786,66 @@ func (d *fixedLenByteArrayDictionary) insertValues(indexes []int32, count int, v
 }
 
 func (d *fixedLenByteArrayDictionary) Lookup(indexes []int32, values []Value) {
-	for i, j := range indexes {
-		values[i] = d.Index(j)
-	}
-}
+	model := d.makeValueString("")
+	memsetValues(values, model)
+	d.lookupString(indexes, makeArrayValue(values), unsafe.Sizeof(model), unsafe.Offsetof(model.ptr))
+}
+
+// func (d *fixedLenByteArrayDictionary) Bounds(indexes []int32) (min, max Value) {
+// 	if len(indexes) > 0 {
+// 		minValue := d.index(indexes[0])
+// 		maxValue := minValue
+
+// 		for _, i := range indexes[1:] {
+// 			value := d.index(i)
+// 			switch {
+// 			case string(value) < string(minValue):
+// 				minValue = value
+// 			case string(value) > string(maxValue):
+// 				maxValue = value
+// 			}
+// 		}
+
+// 		min = d.makeValue(minValue)
+// 		max = d.makeValue(maxValue)
+// 	}
+// 	return min, max
+// }
 
 func (d *fixedLenByteArrayDictionary) Bounds(indexes []int32) (min, max Value) {
 	if len(indexes) > 0 {
-		minValue := d.value(indexes[0])
+		base := d.index(indexes[0])
+		minValue := *(*string)(unsafe.Pointer(&base))
 		maxValue := minValue
+		values := [64]string{}
 
-		for _, i := range indexes[1:] {
-			value := d.value(i)
-			switch {
-			case bytes.Compare(value, minValue) < 0:
-				minValue = value
-			case bytes.Compare(value, maxValue) > 0:
-				maxValue = value
+		for i := 1; i < len(indexes); i += len(values) {
+			n := len(indexes) - i
+			if n > len(values) {
+				n = len(values)
+			}
+			j := i + n
+			d.lookupString(indexes[i:j:j], makeArrayString(values[:n:n]), unsafe.Sizeof(values[0]), 0)
+
+			for _, value := range values[:n:n] {
+				switch {
+				case value < minValue:
+					minValue = value
+				case value > maxValue:
+					maxValue = value
+				}
 			}
 		}
 
-		min = makeValueBytes(FixedLenByteArray, minValue)
-		max = makeValueBytes(FixedLenByteArray, maxValue)
+		min = d.makeValueString(minValue)
+		max = d.makeValueString(maxValue)
 	}
 	return min, max
 }
 
 func (d *fixedLenByteArrayDictionary) Reset() {
 	d.data = d.data[:0]
-	d.index = nil
+	d.hashmap = nil
 }
 
 func (d *fixedLenByteArrayDictionary) Page() BufferedPage {
@@ -813,7 +854,7 @@ func (d *fixedLenByteArrayDictionary) Page() BufferedPage {
 
 type uint32Dictionary struct {
 	uint32Page
-	index map[uint32]int32
+	hashmap map[uint32]int32
 }
 
 func newUint32Dictionary(typ Type, columnIndex int16, numValues int32, data []byte) *uint32Dictionary {
@@ -830,31 +871,33 @@ func (d *uint32Dictionary) Type() Type { return newIndexedType(d.typ, d) }
 
 func (d *uint32Dictionary) Len() int { return len(d.values) }
 
-func (d *uint32Dictionary) Index(i int32) Value { return makeValueUint32(d.values[i]) }
+func (d *uint32Dictionary) Index(i int32) Value { return d.makeValue(d.index(i)) }
+
+func (d *uint32Dictionary) index(i int32) uint32 { return d.values[i] }
 
 func (d *uint32Dictionary) Insert(indexes []int32, values []Value) {
 	var value Value
-	d.insert(indexes, makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
+	d.insert(indexes, makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 }
 
 func (d *uint32Dictionary) insert(indexes []int32, rows array, size, offset uintptr) {
 	_ = indexes[:rows.len]
 
-	if d.index == nil {
-		d.index = make(map[uint32]int32, cap(d.values))
+	if d.hashmap == nil {
+		d.hashmap = make(map[uint32]int32, cap(d.values))
 		for i, v := range d.values {
-			d.index[v] = int32(i)
+			d.hashmap[v] = int32(i)
 		}
 	}
 
 	for i := 0; i < rows.len; i++ {
 		value := *(*uint32)(rows.index(i, size, offset))
 
-		index, exists := d.index[value]
+		index, exists := d.hashmap[value]
 		if !exists {
 			index = int32(len(d.values))
 			d.values = append(d.values, value)
-			d.index[value] = index
+			d.hashmap[value] = index
 		}
 
 		indexes[i] = index
@@ -862,35 +905,23 @@ func (d *uint32Dictionary) insert(indexes []int32, rows array, size, offset uint
 }
 
 func (d *uint32Dictionary) Lookup(indexes []int32, values []Value) {
-	for i, j := range indexes {
-		values[i] = d.Index(j)
-	}
+	model := d.makeValue(0)
+	memsetValues(values, model)
+	d.lookup(indexes, makeArrayValue(values), unsafe.Sizeof(model), unsafe.Offsetof(model.u64))
 }
 
 func (d *uint32Dictionary) Bounds(indexes []int32) (min, max Value) {
 	if len(indexes) > 0 {
-		minValue := d.values[indexes[0]]
-		maxValue := minValue
-
-		for _, i := range indexes[1:] {
-			value := d.values[i]
-			switch {
-			case value < minValue:
-				minValue = value
-			case value > maxValue:
-				maxValue = value
-			}
-		}
-
-		min = makeValueUint32(minValue)
-		max = makeValueUint32(maxValue)
+		minValue, maxValue := d.bounds(indexes)
+		min = d.makeValue(minValue)
+		max = d.makeValue(maxValue)
 	}
 	return min, max
 }
 
 func (d *uint32Dictionary) Reset() {
 	d.values = d.values[:0]
-	d.index = nil
+	d.hashmap = nil
 }
 
 func (d *uint32Dictionary) Page() BufferedPage {
@@ -899,7 +930,7 @@ func (d *uint32Dictionary) Page() BufferedPage {
 
 type uint64Dictionary struct {
 	uint64Page
-	index map[uint64]int32
+	hashmap map[uint64]int32
 }
 
 func newUint64Dictionary(typ Type, columnIndex int16, numValues int32, data []byte) *uint64Dictionary {
@@ -916,31 +947,33 @@ func (d *uint64Dictionary) Type() Type { return newIndexedType(d.typ, d) }
 
 func (d *uint64Dictionary) Len() int { return len(d.values) }
 
-func (d *uint64Dictionary) Index(i int32) Value { return makeValueUint64(d.values[i]) }
+func (d *uint64Dictionary) Index(i int32) Value { return d.makeValue(d.index(i)) }
+
+func (d *uint64Dictionary) index(i int32) uint64 { return d.values[i] }
 
 func (d *uint64Dictionary) Insert(indexes []int32, values []Value) {
 	var value Value
-	d.insert(indexes, makeValueArray(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
+	d.insert(indexes, makeArrayValue(values), unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 }
 
 func (d *uint64Dictionary) insert(indexes []int32, rows array, size, offset uintptr) {
 	_ = indexes[:rows.len]
 
-	if d.index == nil {
-		d.index = make(map[uint64]int32, cap(d.values))
+	if d.hashmap == nil {
+		d.hashmap = make(map[uint64]int32, cap(d.values))
 		for i, v := range d.values {
-			d.index[v] = int32(i)
+			d.hashmap[v] = int32(i)
 		}
 	}
 
 	for i := 0; i < rows.len; i++ {
 		value := *(*uint64)(rows.index(i, size, offset))
 
-		index, exists := d.index[value]
+		index, exists := d.hashmap[value]
 		if !exists {
 			index = int32(len(d.values))
 			d.values = append(d.values, value)
-			d.index[value] = index
+			d.hashmap[value] = index
 		}
 
 		indexes[i] = index
@@ -948,35 +981,23 @@ func (d *uint64Dictionary) insert(indexes []int32, rows array, size, offset uint
 }
 
 func (d *uint64Dictionary) Lookup(indexes []int32, values []Value) {
-	for i, j := range indexes {
-		values[i] = d.Index(j)
-	}
+	model := d.makeValue(0)
+	memsetValues(values, model)
+	d.lookup(indexes, makeArrayValue(values), unsafe.Sizeof(model), unsafe.Offsetof(model.u64))
 }
 
 func (d *uint64Dictionary) Bounds(indexes []int32) (min, max Value) {
 	if len(indexes) > 0 {
-		minValue := d.values[indexes[0]]
-		maxValue := minValue
-
-		for _, i := range indexes[1:] {
-			value := d.values[i]
-			switch {
-			case value < minValue:
-				minValue = value
-			case value > maxValue:
-				maxValue = value
-			}
-		}
-
-		min = makeValueUint64(minValue)
-		max = makeValueUint64(maxValue)
+		minValue, maxValue := d.bounds(indexes)
+		min = d.makeValue(minValue)
+		max = d.makeValue(maxValue)
 	}
 	return min, max
 }
 
 func (d *uint64Dictionary) Reset() {
 	d.values = d.values[:0]
-	d.index = nil
+	d.hashmap = nil
 }
 
 func (d *uint64Dictionary) Page() BufferedPage {
diff --git a/dictionary_amd64.go b/dictionary_amd64.go
new file mode 100644
index 0000000..1b88266
--- /dev/null
+++ b/dictionary_amd64.go
@@ -0,0 +1,117 @@
+//go:build !purego
+
+package parquet
+
+import (
+	"unsafe"
+)
+
+//go:noescape
+func dictionaryBoundsInt32(dict []int32, indexes []int32) (min, max int32, err errno)
+
+//go:noescape
+func dictionaryBoundsInt64(dict []int64, indexes []int32) (min, max int64, err errno)
+
+//go:noescape
+func dictionaryBoundsFloat32(dict []float32, indexes []int32) (min, max float32, err errno)
+
+//go:noescape
+func dictionaryBoundsFloat64(dict []float64, indexes []int32) (min, max float64, err errno)
+
+//go:noescape
+func dictionaryBoundsUint32(dict []uint32, indexes []int32) (min, max uint32, err errno)
+
+//go:noescape
+func dictionaryBoundsUint64(dict []uint64, indexes []int32) (min, max uint64, err errno)
+
+//go:noescape
+func dictionaryLookup32bits(dict []uint32, indexes []int32, rows array, size, offset uintptr) errno
+
+//go:noescape
+func dictionaryLookup64bits(dict []uint64, indexes []int32, rows array, size, offset uintptr) errno
+
+//go:noescape
+func dictionaryLookupByteArrayString(dict []uint32, page []byte, indexes []int32, rows array, size, offset uintptr) errno
+
+//go:noescape
+func dictionaryLookupFixedLenByteArrayString(dict []byte, len int, indexes []int32, rows array, size, offset uintptr) errno
+
+func (d *int32Dictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	dict := *(*[]uint32)(unsafe.Pointer(&d.values))
+	dictionaryLookup32bits(dict, indexes, rows, size, offset).check()
+}
+
+func (d *int64Dictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	dict := *(*[]uint64)(unsafe.Pointer(&d.values))
+	dictionaryLookup64bits(dict, indexes, rows, size, offset).check()
+}
+
+func (d *floatDictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	dict := *(*[]uint32)(unsafe.Pointer(&d.values))
+	dictionaryLookup32bits(dict, indexes, rows, size, offset).check()
+}
+
+func (d *doubleDictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	dict := *(*[]uint64)(unsafe.Pointer(&d.values))
+	dictionaryLookup64bits(dict, indexes, rows, size, offset).check()
+}
+
+func (d *byteArrayDictionary) lookupString(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	dictionaryLookupByteArrayString(d.offsets, d.values, indexes, rows, size, offset).check()
+}
+
+func (d *fixedLenByteArrayDictionary) lookupString(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	dictionaryLookupFixedLenByteArrayString(d.data, d.size, indexes, rows, size, offset).check()
+}
+
+func (d *uint32Dictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	dictionaryLookup32bits(d.values, indexes, rows, size, offset).check()
+}
+
+func (d *uint64Dictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	dictionaryLookup64bits(d.values, indexes, rows, size, offset).check()
+}
+
+func (d *int32Dictionary) bounds(indexes []int32) (min, max int32) {
+	min, max, err := dictionaryBoundsInt32(d.values, indexes)
+	err.check()
+	return min, max
+}
+
+func (d *int64Dictionary) bounds(indexes []int32) (min, max int64) {
+	min, max, err := dictionaryBoundsInt64(d.values, indexes)
+	err.check()
+	return min, max
+}
+
+func (d *floatDictionary) bounds(indexes []int32) (min, max float32) {
+	min, max, err := dictionaryBoundsFloat32(d.values, indexes)
+	err.check()
+	return min, max
+}
+
+func (d *doubleDictionary) bounds(indexes []int32) (min, max float64) {
+	min, max, err := dictionaryBoundsFloat64(d.values, indexes)
+	err.check()
+	return min, max
+}
+
+func (d *uint32Dictionary) bounds(indexes []int32) (min, max uint32) {
+	min, max, err := dictionaryBoundsUint32(d.values, indexes)
+	err.check()
+	return min, max
+}
+
+func (d *uint64Dictionary) bounds(indexes []int32) (min, max uint64) {
+	min, max, err := dictionaryBoundsUint64(d.values, indexes)
+	err.check()
+	return min, max
+}
diff --git a/dictionary_amd64.s b/dictionary_amd64.s
new file mode 100644
index 0000000..64442e2
--- /dev/null
+++ b/dictionary_amd64.s
@@ -0,0 +1,826 @@
+//go:build !purego
+
+#include "textflag.h"
+
+#define errnoIndexOutOfBounds 1
+
+// func dictionaryBoundsInt32(dict []int32, indexes []int32) (min, max int32, err errno)
+TEXT ·dictionaryBoundsInt32(SB), NOSPLIT, $0-64
+    MOVQ dict_base+0(FP), AX
+    MOVQ dict_len+8(FP), BX
+
+    MOVQ indexes_base+24(FP), CX
+    MOVQ indexes_len+32(FP), DX
+
+    XORQ R10, R10 // min
+    XORQ R11, R11 // max
+    XORQ R12, R12 // err
+    XORQ SI, SI
+
+    CMPQ DX, $0
+    JE return
+
+    MOVL (CX), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVL (AX)(DI*4), R10
+    MOVL R10, R11
+
+    CMPQ DX, $8
+    JB test
+
+    CMPB ·hasAVX512VL(SB), $0
+    JE test
+
+    MOVQ DX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+
+    MOVQ $0xFFFF, R8
+    KMOVW R8, K1
+
+    VPBROADCASTD BX, Y2  // [len(dict)...]
+    VPBROADCASTD R10, Y3 // [min...]
+    VMOVDQU32 Y3, Y4     // [max...]
+loopAVX512:
+    VMOVDQU32 (CX)(SI*4), Y0
+    VPCMPUD $1, Y2, Y0, K2
+    KMOVW K2, R9
+    CMPB R9, $0xFF
+    JNE indexOutOfBounds
+    VPGATHERDD (AX)(Y0*4), K1, Y1
+    VPMINSD Y1, Y3, Y3
+    VPMAXSD Y1, Y4, Y4
+    KMOVW R8, K1
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loopAVX512
+
+    VPERM2I128 $1, Y3, Y3, Y0
+    VPERM2I128 $1, Y4, Y4, Y1
+    VPMINSD Y0, Y3, Y3
+    VPMAXSD Y1, Y4, Y4
+
+    VPSHUFD $0b1110, Y3, Y0
+    VPSHUFD $0b1110, Y4, Y1
+    VPMINSD Y0, Y3, Y3
+    VPMAXSD Y1, Y4, Y4
+
+    VPSHUFD $1, Y3, Y0
+    VPSHUFD $1, Y4, Y1
+    VPMINSD Y0, Y3, Y3
+    VPMAXSD Y1, Y4, Y4
+
+    MOVQ X3, R10
+    MOVQ X4, R11
+    ANDQ $0xFFFFFFFF, R10
+    ANDQ $0xFFFFFFFF, R11
+
+    VZEROUPPER
+    JMP test
+loop:
+    MOVL (CX)(SI*4), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVL (AX)(DI*4), DI
+    CMPL DI, R10
+    CMOVLLT DI, R10
+    CMPL DI, R11
+    CMOVLGT DI, R11
+    INCQ SI
+test:
+    CMPQ SI, DX
+    JNE loop
+return:
+    MOVL R10, min+48(FP)
+    MOVL R11, max+52(FP)
+    MOVQ R12, err+56(FP)
+    RET
+indexOutOfBounds:
+    MOVQ $errnoIndexOutOfBounds, R12
+    JMP return
+
+// func dictionaryBoundsInt64(dict []int64, indexes []int32) (min, max int64, err errno)
+TEXT ·dictionaryBoundsInt64(SB), NOSPLIT, $0-72
+    MOVQ dict_base+0(FP), AX
+    MOVQ dict_len+8(FP), BX
+
+    MOVQ indexes_base+24(FP), CX
+    MOVQ indexes_len+32(FP), DX
+
+    XORQ R10, R10 // min
+    XORQ R11, R11 // max
+    XORQ R12, R12 // err
+    XORQ SI, SI
+
+    CMPQ DX, $0
+    JE return
+
+    MOVL (CX), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVQ (AX)(DI*8), R10
+    MOVQ R10, R11
+
+    CMPQ DX, $8
+    JB test
+
+    CMPB ·hasAVX512VL(SB), $0
+    JE test
+
+    MOVQ DX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+
+    MOVQ $0xFFFF, R8
+    KMOVW R8, K1
+
+    VPBROADCASTD BX, Y2  // [len(dict)...]
+    VPBROADCASTQ R10, Z3 // [min...]
+    VMOVDQU64 Z3, Z4     // [max...]
+loopAVX512:
+    VMOVDQU32 (CX)(SI*4), Y0
+    VPCMPUD $1, Y2, Y0, K2
+    KMOVW K2, R9
+    CMPB R9, $0xFF
+    JNE indexOutOfBounds
+    VPGATHERDQ (AX)(Y0*8), K1, Z1
+    VPMINSQ Z1, Z3, Z3
+    VPMAXSQ Z1, Z4, Z4
+    KMOVW R8, K1
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loopAVX512
+
+    VPERMQ $0b1110, Z3, Z0
+    VPERMQ $0b1110, Z4, Z1
+    VPMINSQ Z0, Z3, Z3
+    VPMAXSQ Z1, Z4, Z4
+
+    VPERMQ $1, Z3, Z0
+    VPERMQ $1, Z4, Z1
+    VPMINSQ Z0, Z3, Z3
+    VPMAXSQ Z1, Z4, Z4
+
+    VSHUFF64X2 $2, Z3, Z3, Z0
+    VSHUFF64X2 $2, Z4, Z4, Z1
+    VPMINSQ Z0, Z3, Z3
+    VPMAXSQ Z1, Z4, Z4
+
+    MOVQ X3, R10
+    MOVQ X4, R11
+
+    VZEROUPPER
+    JMP test
+loop:
+    MOVL (CX)(SI*4), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVQ (AX)(DI*8), DI
+    CMPQ DI, R10
+    CMOVQLT DI, R10
+    CMPQ DI, R11
+    CMOVQGT DI, R11
+    INCQ SI
+test:
+    CMPQ SI, DX
+    JNE loop
+return:
+    MOVQ R10, min+48(FP)
+    MOVQ R11, max+56(FP)
+    MOVQ R12, err+64(FP)
+    RET
+indexOutOfBounds:
+    MOVQ $errnoIndexOutOfBounds, R12
+    JMP return
+
+// func dictionaryBoundsFloat32(dict []float32, indexes []int32) (min, max float32, err errno)
+TEXT ·dictionaryBoundsFloat32(SB), NOSPLIT, $0-64
+    MOVQ dict_base+0(FP), AX
+    MOVQ dict_len+8(FP), BX
+
+    MOVQ indexes_base+24(FP), CX
+    MOVQ indexes_len+32(FP), DX
+
+    PXOR X3, X3   // min
+    PXOR X4, X4   // max
+    XORQ R12, R12 // err
+    XORQ SI, SI
+
+    CMPQ DX, $0
+    JE return
+
+    MOVL (CX), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVSS (AX)(DI*4), X3
+    MOVAPS X3, X4
+
+    CMPQ DX, $8
+    JB test
+
+    CMPB ·hasAVX512VL(SB), $0
+    JE test
+
+    MOVQ DX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+
+    MOVQ $0xFFFF, R8
+    KMOVW R8, K1
+
+    VPBROADCASTD BX, Y2 // [len(dict)...]
+    VPBROADCASTD X3, Y3 // [min...]
+    VMOVDQU32 Y3, Y4    // [max...]
+loopAVX512:
+    VMOVDQU32 (CX)(SI*4), Y0
+    VPCMPUD $1, Y2, Y0, K2
+    KMOVW K2, R9
+    CMPB R9, $0xFF
+    JNE indexOutOfBounds
+    VPGATHERDD (AX)(Y0*4), K1, Y1
+    VMINPS Y1, Y3, Y3
+    VMAXPS Y1, Y4, Y4
+    KMOVW R8, K1
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loopAVX512
+
+    VPERM2I128 $1, Y3, Y3, Y0
+    VPERM2I128 $1, Y4, Y4, Y1
+    VMINPS Y0, Y3, Y3
+    VMAXPS Y1, Y4, Y4
+
+    VPSHUFD $0b1110, Y3, Y0
+    VPSHUFD $0b1110, Y4, Y1
+    VMINPS Y0, Y3, Y3
+    VMAXPS Y1, Y4, Y4
+
+    VPSHUFD $1, Y3, Y0
+    VPSHUFD $1, Y4, Y1
+    VMINPS Y0, Y3, Y3
+    VMAXPS Y1, Y4, Y4
+
+    VZEROUPPER
+    JMP test
+loop:
+    MOVL (CX)(SI*4), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVSS (AX)(DI*4), X1
+    UCOMISS X3, X1
+    JAE skipAssignMin
+    MOVAPS X1, X3
+skipAssignMin:
+    UCOMISS X4, X1
+    JBE skipAssignMax
+    MOVAPS X1, X4
+skipAssignMax:
+    INCQ SI
+test:
+    CMPQ SI, DX
+    JNE loop
+return:
+    MOVSS X3, min+48(FP)
+    MOVSS X4, max+52(FP)
+    MOVQ R12, err+56(FP)
+    RET
+indexOutOfBounds:
+    MOVQ $errnoIndexOutOfBounds, R12
+    JMP return
+
+// func dictionaryBoundsFloat64(dict []float64, indexes []int32) (min, max float64, err errno)
+TEXT ·dictionaryBoundsFloat64(SB), NOSPLIT, $0-72
+    MOVQ dict_base+0(FP), AX
+    MOVQ dict_len+8(FP), BX
+
+    MOVQ indexes_base+24(FP), CX
+    MOVQ indexes_len+32(FP), DX
+
+    PXOR X3, X3   // min
+    PXOR X4, X4   // max
+    XORQ R12, R12 // err
+    XORQ SI, SI
+
+    CMPQ DX, $0
+    JE return
+
+    MOVL (CX), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVSD (AX)(DI*8), X3
+    MOVAPS X3, X4
+
+    CMPQ DX, $8
+    JB test
+
+    CMPB ·hasAVX512VL(SB), $0
+    JE test
+
+    MOVQ DX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+
+    MOVQ $0xFFFF, R8
+    KMOVW R8, K1
+
+    VPBROADCASTD BX, Y2 // [len(dict)...]
+    VPBROADCASTQ X3, Z3 // [min...]
+    VMOVDQU64 Z3, Z4    // [max...]
+loopAVX512:
+    VMOVDQU32 (CX)(SI*4), Y0
+    VPCMPUD $1, Y2, Y0, K2
+    KMOVW K2, R9
+    CMPB R9, $0xFF
+    JNE indexOutOfBounds
+    VPGATHERDQ (AX)(Y0*8), K1, Z1
+    VMINPD Z1, Z3, Z3
+    VMAXPD Z1, Z4, Z4
+    KMOVW R8, K1
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loopAVX512
+
+    VPERMQ $0b1110, Z3, Z0
+    VPERMQ $0b1110, Z4, Z1
+    VMINPD Z0, Z3, Z3
+    VMAXPD Z1, Z4, Z4
+
+    VPERMQ $1, Z3, Z0
+    VPERMQ $1, Z4, Z1
+    VMINPD Z0, Z3, Z3
+    VMAXPD Z1, Z4, Z4
+
+    VSHUFF64X2 $2, Z3, Z3, Z0
+    VSHUFF64X2 $2, Z4, Z4, Z1
+    VMINPD Z0, Z3, Z3
+    VMAXPD Z1, Z4, Z4
+
+    VZEROUPPER
+    JMP test
+loop:
+    MOVL (CX)(SI*4), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVSD (AX)(DI*8), X1
+    UCOMISD X3, X1
+    JAE skipAssignMin
+    MOVAPD X1, X3
+skipAssignMin:
+    UCOMISD X4, X1
+    JBE skipAssignMax
+    MOVAPD X1, X4
+skipAssignMax:
+    INCQ SI
+test:
+    CMPQ SI, DX
+    JNE loop
+return:
+    MOVSD X3, min+48(FP)
+    MOVSD X4, max+56(FP)
+    MOVQ R12, err+64(FP)
+    RET
+indexOutOfBounds:
+    MOVQ $errnoIndexOutOfBounds, R12
+    JMP return
+
+// func dictionaryBoundsUint32(dict []uint32, indexes []int32) (min, max uint32, err errno)
+TEXT ·dictionaryBoundsUint32(SB), NOSPLIT, $0-64
+    MOVQ dict_base+0(FP), AX
+    MOVQ dict_len+8(FP), BX
+
+    MOVQ indexes_base+24(FP), CX
+    MOVQ indexes_len+32(FP), DX
+
+    XORQ R10, R10 // min
+    XORQ R11, R11 // max
+    XORQ R12, R12 // err
+    XORQ SI, SI
+
+    CMPQ DX, $0
+    JE return
+
+    MOVL (CX), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVL (AX)(DI*4), R10
+    MOVL R10, R11
+
+    CMPQ DX, $8
+    JB test
+
+    CMPB ·hasAVX512VL(SB), $0
+    JE test
+
+    MOVQ DX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+
+    MOVQ $0xFFFF, R8
+    KMOVW R8, K1
+
+    VPBROADCASTD BX, Y2  // [len(dict)...]
+    VPBROADCASTD R10, Y3 // [min...]
+    VMOVDQU32 Y3, Y4     // [max...]
+loopAVX512:
+    VMOVDQU32 (CX)(SI*4), Y0
+    VPCMPUD $1, Y2, Y0, K2
+    KMOVW K2, R9
+    CMPB R9, $0xFF
+    JNE indexOutOfBounds
+    VPGATHERDD (AX)(Y0*4), K1, Y1
+    VPMINUD Y1, Y3, Y3
+    VPMAXUD Y1, Y4, Y4
+    KMOVW R8, K1
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loopAVX512
+
+    VPERM2I128 $1, Y3, Y3, Y0
+    VPERM2I128 $1, Y4, Y4, Y1
+    VPMINUD Y0, Y3, Y3
+    VPMAXUD Y1, Y4, Y4
+
+    VPSHUFD $0b1110, Y3, Y0
+    VPSHUFD $0b1110, Y4, Y1
+    VPMINUD Y0, Y3, Y3
+    VPMAXUD Y1, Y4, Y4
+
+    VPSHUFD $1, Y3, Y0
+    VPSHUFD $1, Y4, Y1
+    VPMINUD Y0, Y3, Y3
+    VPMAXUD Y1, Y4, Y4
+
+    MOVQ X3, R10
+    MOVQ X4, R11
+    ANDQ $0xFFFFFFFF, R10
+    ANDQ $0xFFFFFFFF, R11
+
+    VZEROUPPER
+    JMP test
+loop:
+    MOVL (CX)(SI*4), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVL (AX)(DI*4), DI
+    CMPL DI, R10
+    CMOVLCS DI, R10
+    CMPL DI, R11
+    CMOVLHI DI, R11
+    INCQ SI
+test:
+    CMPQ SI, DX
+    JNE loop
+return:
+    MOVL R10, min+48(FP)
+    MOVL R11, max+52(FP)
+    MOVQ R12, err+56(FP)
+    RET
+indexOutOfBounds:
+    MOVQ $errnoIndexOutOfBounds, R12
+    JMP return
+
+// func dictionaryBoundsUint64(dict []uint64, indexes []int32) (min, max uint64, err errno)
+TEXT ·dictionaryBoundsUint64(SB), NOSPLIT, $0-72
+    MOVQ dict_base+0(FP), AX
+    MOVQ dict_len+8(FP), BX
+
+    MOVQ indexes_base+24(FP), CX
+    MOVQ indexes_len+32(FP), DX
+
+    XORQ R10, R10 // min
+    XORQ R11, R11 // max
+    XORQ R12, R12 // err
+    XORQ SI, SI
+
+    CMPQ DX, $0
+    JE return
+
+    MOVL (CX)(SI*4), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVQ (AX)(DI*8), R10
+    MOVQ R10, R11
+
+    CMPQ DX, $8
+    JB test
+
+    CMPB ·hasAVX512VL(SB), $0
+    JE test
+
+    MOVQ DX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+
+    MOVQ $0xFFFF, R8
+    KMOVW R8, K1
+
+    VPBROADCASTD BX, Y2  // [len(dict)...]
+    VPBROADCASTQ R10, Z3 // [min...]
+    VMOVDQU64 Z3, Z4     // [max...]
+loopAVX512:
+    VMOVDQU32 (CX)(SI*4), Y0
+    VPCMPUD $1, Y2, Y0, K2
+    KMOVW K2, R9
+    CMPB R9, $0xFF
+    JNE indexOutOfBounds
+    VPGATHERDQ (AX)(Y0*8), K1, Z1
+    VPMINUQ Z1, Z3, Z3
+    VPMAXUQ Z1, Z4, Z4
+    KMOVW R8, K1
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loopAVX512
+
+    VPERMQ $0b1110, Z3, Z0
+    VPERMQ $0b1110, Z4, Z1
+    VPMINUQ Z0, Z3, Z3
+    VPMAXUQ Z1, Z4, Z4
+
+    VPERMQ $1, Z3, Z0
+    VPERMQ $1, Z4, Z1
+    VPMINUQ Z0, Z3, Z3
+    VPMAXUQ Z1, Z4, Z4
+
+    VSHUFF64X2 $2, Z3, Z3, Z0
+    VSHUFF64X2 $2, Z4, Z4, Z1
+    VPMINUQ Z0, Z3, Z3
+    VPMAXUQ Z1, Z4, Z4
+
+    MOVQ X3, R10
+    MOVQ X4, R11
+
+    VZEROUPPER
+    JMP test
+loop:
+    MOVL (CX)(SI*4), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVQ (AX)(DI*8), DI
+    CMPQ DI, R10
+    CMOVQCS DI, R10
+    CMPQ DI, R11
+    CMOVQHI DI, R11
+    INCQ SI
+test:
+    CMPQ SI, DX
+    JNE loop
+return:
+    MOVQ R10, min+48(FP)
+    MOVQ R11, max+56(FP)
+    MOVQ R12, err+64(FP)
+    RET
+indexOutOfBounds:
+    MOVQ $errnoIndexOutOfBounds, R12
+    JMP return
+
+// The lookup functions provide optimized versions of the dictionary index
+// lookup logic.
+//
+// When AVX512 is available, the AVX512 versions of the functions are used
+// which use the VPGATHER* instructions to perform 8 parallel lookups of the
+// values in the dictionary, then VPSCATTER* to do 8 parallel writes to the
+// sparse output buffer.
+
+// func dictionaryLookup32bits(dict []uint32, indexes []int32, rows array, size, offset uintptr) errno
+TEXT ·dictionaryLookup32bits(SB), NOSPLIT, $0-88
+    MOVQ dict_base+0(FP), AX
+    MOVQ dict_len+8(FP), BX
+
+    MOVQ indexes_base+24(FP), CX
+    MOVQ indexes_len+32(FP), DX
+
+    MOVQ rows_ptr+48(FP), R8
+    MOVQ size+64(FP), R9
+    ADDQ offset+72(FP), R8
+
+    XORQ SI, SI
+
+    CMPQ DX, $8
+    JB test
+
+    CMPB ·hasAVX512VL(SB), $0
+    JE test
+
+    MOVQ DX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+
+    MOVQ R9, R10
+    SHLQ $3, R10 // 8 * size
+
+    MOVW $0xFFFF, R11
+    KMOVW R11, K1
+    KMOVW R11, K2
+
+    VPBROADCASTD R9, Y2           // [size...]
+    VPMULLD ·range0n8(SB), Y2, Y2 // [0*size,1*size,...]
+    VPBROADCASTD BX, Y3           // [len(dict)...]
+loopAVX512:
+    VMOVDQU32 (CX)(SI*4), Y0
+    VPCMPUD $1, Y3, Y0, K3
+    KMOVW K3, R11
+    CMPB R11, $0xFF
+    JNE indexOutOfBounds
+    VPGATHERDD (AX)(Y0*4), K1, Y1
+    VPSCATTERDD Y1, K2, (R8)(Y2*1)
+    KMOVW R11, K1
+    KMOVW R11, K2
+    ADDQ R10, R8
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loopAVX512
+    VZEROUPPER
+    JMP test
+loop:
+    MOVL (CX)(SI*4), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVL (AX)(DI*4), DI
+    MOVL DI, (R8)
+    ADDQ R9, R8
+    INCQ SI
+test:
+    CMPQ SI, DX
+    JNE loop
+    XORQ AX, AX
+return:
+    MOVQ AX, ret+80(FP)
+    RET
+indexOutOfBounds:
+    MOVQ $errnoIndexOutOfBounds, AX
+    JMP return
+
+// func dictionaryLookup64bits(dict []uint64, indexes []int32, rows array, size, offset uintptr) errno
+TEXT ·dictionaryLookup64bits(SB), NOSPLIT, $0-88
+    MOVQ dict_base+0(FP), AX
+    MOVQ dict_len+8(FP), BX
+
+    MOVQ indexes_base+24(FP), CX
+    MOVQ indexes_len+32(FP), DX
+
+    MOVQ rows_ptr+48(FP), R8
+    MOVQ size+64(FP), R9
+    ADDQ offset+72(FP), R8
+
+    XORQ SI, SI
+
+    CMPQ DX, $8
+    JB test
+
+    CMPB ·hasAVX512VL(SB), $0
+    JE test
+
+    MOVQ DX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+
+    MOVQ R9, R10
+    SHLQ $3, R10 // 8 * size
+
+    MOVW $0xFFFF, R11
+    KMOVW R11, K1
+    KMOVW R11, K2
+
+    VPBROADCASTD R9, Y2           // [size...]
+    VPMULLD ·range0n8(SB), Y2, Y2 // [0*size,1*size,...]
+    VPBROADCASTD BX, Y3           // [len(dict)...]
+loopAVX512:
+    VMOVDQU32 (CX)(SI*4), Y0
+    VPCMPUD $1, Y3, Y0, K3
+    KMOVW K3, R11
+    CMPB R11, $0xFF
+    JNE indexOutOfBounds
+    VPGATHERDQ (AX)(Y0*8), K1, Z1
+    VPSCATTERDQ Z1, K2, (R8)(Y2*1)
+    KMOVW R11, K1
+    KMOVW R11, K2
+    ADDQ R10, R8
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loopAVX512
+    VZEROUPPER
+    JMP test
+loop:
+    MOVL (CX)(SI*4), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+    MOVQ (AX)(DI*8), DI
+    MOVQ DI, (R8)
+    ADDQ R9, R8
+    INCQ SI
+test:
+    CMPQ SI, DX
+    JNE loop
+    XORQ AX, AX
+return:
+    MOVQ AX, ret+80(FP)
+    RET
+indexOutOfBounds:
+    MOVQ $errnoIndexOutOfBounds, AX
+    JMP return
+
+// func dictionaryLookupByteArrayString(dict []uint32, page []byte, indexes []int32, rows array, size, offset uintptr) errno
+TEXT ·dictionaryLookupByteArrayString(SB), NOSPLIT, $0-112
+    MOVQ dict_base+0(FP), AX
+    MOVQ dict_len+8(FP), BX
+
+    MOVQ page+24(FP), CX
+
+    MOVQ indexes_base+48(FP), R8
+    MOVQ indexes_len+56(FP), R9
+
+    MOVQ rows_ptr+72(FP), R10
+    MOVQ size+88(FP), R11
+    ADDQ offset+96(FP), R10
+
+    XORQ DI, DI
+    XORQ SI, SI
+loop:
+    // Load the index that we want to read the value from. This may come from
+    // user input so we must validate that the indexes are within the bounds of
+    // the dictionary.
+    MOVL (R8)(SI*4), DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+
+    // Load the offset within the dictionary page where the value is stored.
+    // We trust the offsets to be correct since they are generated internally by
+    // the dictionary code, there is no need to check that they are within the
+    // bounds of the dictionary page.
+    MOVL (AX)(DI*4), DI
+
+    // Load the value from the dictionary page. The page uses the PLAIN encoding
+    // where each byte array is prefixed with a 4 bytes little endian length.
+    LEAQ 4(CX)(DI*1), DX
+    MOVL (CX)(DI*1), DI
+
+    // Store the length and pointer to the value into the output location.
+    // The memory layout is expected to hold a pointer and length, which are
+    // both 64 bits words. This is the layout used by parquet.Value and the Go
+    // string value type.
+    MOVQ DX, (R10)
+    MOVQ DI, 8(R10)
+
+    ADDQ R11, R10
+    INCQ SI
+test:
+    CMPQ SI, R9
+    JNE loop
+    XORQ AX, AX
+return:
+    MOVQ AX, ret+104(FP)
+    RET
+indexOutOfBounds:
+    MOVQ $errnoIndexOutOfBounds, AX
+    JMP return
+
+// func dictionaryLookupFixedLenByteArrayString(dict []byte, len int, indexes []int32, rows array, size, offset uintptr) errno
+TEXT ·dictionaryLookupFixedLenByteArrayString(SB), NOSPLIT, $0-96
+    MOVQ dict_base+0(FP), AX
+    MOVQ dict_len+8(FP), BX
+
+    MOVQ len+24(FP), CX
+
+    MOVQ indexes_base+32(FP), DX
+    MOVQ indexes_len+40(FP), R8
+
+    MOVQ rows_ptr+56(FP), R9
+    MOVQ size+72(FP), R10
+    ADDQ offset+80(FP), R9
+
+    XORQ DI, DI
+    XORQ SI, SI
+loop:
+    MOVL (DX)(SI*4), DI
+    IMULQ CX, DI
+    CMPL DI, BX
+    JAE indexOutOfBounds
+
+    ADDQ AX, DI
+    MOVQ DI, (R9)
+    MOVQ CX, 8(R9)
+
+    ADDQ R10, R9
+    INCQ SI
+test:
+    CMPQ SI, R8
+    JNE loop
+    XORQ AX, AX
+return:
+    MOVQ AX, ret+88(FP)
+    RET
+indexOutOfBounds:
+    MOVQ $errnoIndexOutOfBounds, AX
+    JMP return
+
+GLOBL ·range0n8(SB), RODATA|NOPTR, $40
+DATA ·range0n8+0(SB)/4, $0
+DATA ·range0n8+4(SB)/4, $1
+DATA ·range0n8+8(SB)/4, $2
+DATA ·range0n8+12(SB)/4, $3
+DATA ·range0n8+16(SB)/4, $4
+DATA ·range0n8+20(SB)/4, $5
+DATA ·range0n8+24(SB)/4, $6
+DATA ·range0n8+28(SB)/4, $7
+DATA ·range0n8+32(SB)/4, $8
diff --git a/dictionary_purego.go b/dictionary_purego.go
new file mode 100644
index 0000000..ec93f78
--- /dev/null
+++ b/dictionary_purego.go
@@ -0,0 +1,165 @@
+//go:build purego || !amd64
+
+package parquet
+
+import "unsafe"
+
+func (d *int32Dictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	for i, j := range indexes {
+		*(*int32)(rows.index(i, size, offset)) = d.index(j)
+	}
+}
+
+func (d *int64Dictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	for i, j := range indexes {
+		*(*int64)(rows.index(i, size, offset)) = d.index(j)
+	}
+}
+
+func (d *floatDictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	for i, j := range indexes {
+		*(*float32)(rows.index(i, size, offset)) = d.index(j)
+	}
+}
+
+func (d *doubleDictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	for i, j := range indexes {
+		*(*float64)(rows.index(i, size, offset)) = d.index(j)
+	}
+}
+
+func (d *byteArrayDictionary) lookupString(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	for i, j := range indexes {
+		v := d.index(j)
+		*(*string)(rows.index(i, size, offset)) = *(*string)(unsafe.Pointer(&v))
+	}
+}
+
+func (d *fixedLenByteArrayDictionary) lookupString(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	for i, j := range indexes {
+		v := d.index(j)
+		*(*string)(rows.index(i, size, offset)) = *(*string)(unsafe.Pointer(&v))
+	}
+}
+
+func (d *uint32Dictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	for i, j := range indexes {
+		*(*uint32)(rows.index(i, size, offset)) = d.index(j)
+	}
+}
+
+func (d *uint64Dictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
+	checkLookupIndexBounds(indexes, rows)
+	for i, j := range indexes {
+		*(*uint64)(rows.index(i, size, offset)) = d.index(j)
+	}
+}
+
+func (d *int32Dictionary) bounds(indexes []int32) (min, max int32) {
+	min = d.index(indexes[0])
+	max = min
+
+	for _, i := range indexes[1:] {
+		value := d.index(i)
+		if value < min {
+			min = value
+		}
+		if value > max {
+			max = value
+		}
+	}
+
+	return min, max
+}
+
+func (d *int64Dictionary) bounds(indexes []int32) (min, max int64) {
+	min = d.index(indexes[0])
+	max = min
+
+	for _, i := range indexes[1:] {
+		value := d.index(i)
+		if value < min {
+			min = value
+		}
+		if value > max {
+			max = value
+		}
+	}
+
+	return min, max
+}
+
+func (d *floatDictionary) bounds(indexes []int32) (min, max float32) {
+	min = d.index(indexes[0])
+	max = min
+
+	for _, i := range indexes[1:] {
+		value := d.index(i)
+		if value < min {
+			min = value
+		}
+		if value > max {
+			max = value
+		}
+	}
+
+	return min, max
+}
+
+func (d *doubleDictionary) bounds(indexes []int32) (min, max float64) {
+	min = d.index(indexes[0])
+	max = min
+
+	for _, i := range indexes[1:] {
+		value := d.index(i)
+		if value < min {
+			min = value
+		}
+		if value > max {
+			max = value
+		}
+	}
+
+	return min, max
+}
+
+func (d *uint32Dictionary) bounds(indexes []int32) (min, max uint32) {
+	min = d.index(indexes[0])
+	max = min
+
+	for _, i := range indexes[1:] {
+		value := d.index(i)
+		if value < min {
+			min = value
+		}
+		if value > max {
+			max = value
+		}
+	}
+
+	return min, max
+}
+
+func (d *uint64Dictionary) bounds(indexes []int32) (min, max uint64) {
+	min = d.index(indexes[0])
+	max = min
+
+	for _, i := range indexes[1:] {
+		value := d.index(i)
+		if value < min {
+			min = value
+		}
+		if value > max {
+			max = value
+		}
+	}
+
+	return min, max
+}
diff --git a/errors.go b/errors.go
index 5da5ccb..69ddf94 100644
--- a/errors.go
+++ b/errors.go
@@ -50,3 +50,20 @@ var (
 	// decode definition levels into a page which is part of a required column.
 	ErrUnexpectedDefinitionLevels = errors.New("unexpected definition levels")
 )
+
+type errno int
+
+const (
+	ok errno = iota
+	indexOutOfBounds
+)
+
+func (e errno) check() {
+	switch e {
+	case ok:
+	case indexOutOfBounds:
+		panic("index out of bounds")
+	default:
+		panic("BUG: unknown error code")
+	}
+}
diff --git a/level_amd64.s b/level_amd64.s
index 4f071fd..872f49c 100644
--- a/level_amd64.s
+++ b/level_amd64.s
@@ -3,9 +3,9 @@
 #include "textflag.h"
 
 // func memsetAVX2(dst []byte, src byte)
-TEXT ·memsetAVX2(SB), NOSPLIT, $0-32
-    MOVQ dst+0(FP), AX
-    MOVQ dst+8(FP), BX
+TEXT ·memsetAVX2(SB), NOSPLIT, $0-25
+    MOVQ dst_base+0(FP), AX
+    MOVQ dst_len+8(FP), BX
     MOVBQZX src+24(FP), CX
 
     CMPQ BX, $8
diff --git a/null_amd64.s b/null_amd64.s
index 6c45b6f..20b85fb 100644
--- a/null_amd64.s
+++ b/null_amd64.s
@@ -5,8 +5,8 @@
 // func nullIndex8bits(bits *uint64, rows array, size, offset uintptr)
 TEXT ·nullIndex8bits(SB), NOSPLIT, $0-40
     MOVQ bits+0(FP), AX
-    MOVQ rows+8(FP), BX
-    MOVQ rows+16(FP), DI
+    MOVQ rows_ptr+8(FP), BX
+    MOVQ rows_len+16(FP), DI
     MOVQ size+24(FP), DX
     ADDQ offset+32(FP), BX
 
@@ -36,8 +36,8 @@ done:
 // func nullIndex32bits(bits *uint64, rows array, size, offset uintptr)
 TEXT ·nullIndex32bits(SB), NOSPLIT, $0-40
     MOVQ bits+0(FP), AX
-    MOVQ rows+8(FP), BX
-    MOVQ rows+16(FP), DI
+    MOVQ rows_ptr+8(FP), BX
+    MOVQ rows_len+16(FP), DI
     MOVQ size+24(FP), DX
     ADDQ offset+32(FP), BX
 
@@ -58,7 +58,7 @@ TEXT ·nullIndex32bits(SB), NOSPLIT, $0-40
     SHLQ $3, R8
 
     VPBROADCASTD size+24(FP), Y0
-    VPMULLD ·scale8x4(SB), Y0, Y0
+    VPMULLD ·range0n8(SB), Y0, Y0
     VPCMPEQD Y1, Y1, Y1
     VPCMPEQD Y2, Y2, Y2
     VPXOR Y3, Y3, Y3
@@ -115,8 +115,8 @@ done:
 // func nullIndex64bits(bits *uint64, rows array, size, offset uintptr)
 TEXT ·nullIndex64bits(SB), NOSPLIT, $0-40
     MOVQ bits+0(FP), AX
-    MOVQ rows+8(FP), BX
-    MOVQ rows+16(FP), DI
+    MOVQ rows_ptr+8(FP), BX
+    MOVQ rows_len+16(FP), DI
     MOVQ size+24(FP), DX
     ADDQ offset+32(FP), BX
 
@@ -194,8 +194,8 @@ done:
 // func nullIndex128bits(bits *uint64, rows array, size, offset uintptr)
 TEXT ·nullIndex128bits(SB), NOSPLIT, $0-40
     MOVQ bits+0(FP), AX
-    MOVQ rows+8(FP), BX
-    MOVQ rows+16(FP), DI
+    MOVQ rows_ptr+8(FP), BX
+    MOVQ rows_len+16(FP), DI
     MOVQ size+24(FP), DX
     ADDQ offset+32(FP), BX
 
diff --git a/page.go b/page.go
index a3aae73..987344a 100644
--- a/page.go
+++ b/page.go
@@ -542,8 +542,8 @@ func (page *booleanPage) bounds() (min, max bool) {
 func (page *booleanPage) Bounds() (min, max Value, ok bool) {
 	if ok = page.numValues > 0; ok {
 		minBool, maxBool := page.bounds()
-		min = makeValueBoolean(minBool)
-		max = makeValueBoolean(maxBool)
+		min = page.makeValue(minBool)
+		max = page.makeValue(maxBool)
 	}
 	return min, max, ok
 }
@@ -575,6 +575,12 @@ func (page *booleanPage) Slice(i, j int64) BufferedPage {
 	}
 }
 
+func (page *booleanPage) makeValue(v bool) Value {
+	value := makeValueBoolean(v)
+	value.columnIndex = page.columnIndex
+	return value
+}
+
 type int32Page struct {
 	typ         Type
 	values      []int32
@@ -622,8 +628,8 @@ func (page *int32Page) bounds() (min, max int32) { return bits.MinMaxInt32(page.
 func (page *int32Page) Bounds() (min, max Value, ok bool) {
 	if ok = len(page.values) > 0; ok {
 		minInt32, maxInt32 := page.bounds()
-		min = makeValueInt32(minInt32)
-		max = makeValueInt32(maxInt32)
+		min = page.makeValue(minInt32)
+		max = page.makeValue(maxInt32)
 	}
 	return min, max, ok
 }
@@ -644,6 +650,12 @@ func (page *int32Page) Slice(i, j int64) BufferedPage {
 	}
 }
 
+func (page *int32Page) makeValue(v int32) Value {
+	value := makeValueInt32(v)
+	value.columnIndex = page.columnIndex
+	return value
+}
+
 type int64Page struct {
 	typ         Type
 	values      []int64
@@ -691,8 +703,8 @@ func (page *int64Page) bounds() (min, max int64) { return bits.MinMaxInt64(page.
 func (page *int64Page) Bounds() (min, max Value, ok bool) {
 	if ok = len(page.values) > 0; ok {
 		minInt64, maxInt64 := page.bounds()
-		min = makeValueInt64(minInt64)
-		max = makeValueInt64(maxInt64)
+		min = page.makeValue(minInt64)
+		max = page.makeValue(maxInt64)
 	}
 	return min, max, ok
 }
@@ -713,6 +725,12 @@ func (page *int64Page) Slice(i, j int64) BufferedPage {
 	}
 }
 
+func (page *int64Page) makeValue(v int64) Value {
+	value := makeValueInt64(v)
+	value.columnIndex = page.columnIndex
+	return value
+}
+
 type int96Page struct {
 	typ         Type
 	values      []deprecated.Int96
@@ -762,8 +780,8 @@ func (page *int96Page) bounds() (min, max deprecated.Int96) {
 func (page *int96Page) Bounds() (min, max Value, ok bool) {
 	if ok = len(page.values) > 0; ok {
 		minInt96, maxInt96 := page.bounds()
-		min = makeValueInt96(minInt96)
-		max = makeValueInt96(maxInt96)
+		min = page.makeValue(minInt96)
+		max = page.makeValue(maxInt96)
 	}
 	return min, max, ok
 }
@@ -784,6 +802,12 @@ func (page *int96Page) Slice(i, j int64) BufferedPage {
 	}
 }
 
+func (page *int96Page) makeValue(v deprecated.Int96) Value {
+	value := makeValueInt96(v)
+	value.columnIndex = page.columnIndex
+	return value
+}
+
 type floatPage struct {
 	typ         Type
 	values      []float32
@@ -831,8 +855,8 @@ func (page *floatPage) bounds() (min, max float32) { return bits.MinMaxFloat32(p
 func (page *floatPage) Bounds() (min, max Value, ok bool) {
 	if ok = len(page.values) > 0; ok {
 		minFloat32, maxFloat32 := page.bounds()
-		min = makeValueFloat(minFloat32)
-		max = makeValueFloat(maxFloat32)
+		min = page.makeValue(minFloat32)
+		max = page.makeValue(maxFloat32)
 	}
 	return min, max, ok
 }
@@ -853,6 +877,12 @@ func (page *floatPage) Slice(i, j int64) BufferedPage {
 	}
 }
 
+func (page *floatPage) makeValue(v float32) Value {
+	value := makeValueFloat(v)
+	value.columnIndex = page.columnIndex
+	return value
+}
+
 type doublePage struct {
 	typ         Type
 	values      []float64
@@ -900,8 +930,8 @@ func (page *doublePage) bounds() (min, max float64) { return bits.MinMaxFloat64(
 func (page *doublePage) Bounds() (min, max Value, ok bool) {
 	if ok = len(page.values) > 0; ok {
 		minFloat64, maxFloat64 := page.bounds()
-		min = makeValueDouble(minFloat64)
-		max = makeValueDouble(maxFloat64)
+		min = page.makeValue(minFloat64)
+		max = page.makeValue(maxFloat64)
 	}
 	return min, max, ok
 }
@@ -922,6 +952,12 @@ func (page *doublePage) Slice(i, j int64) BufferedPage {
 	}
 }
 
+func (page *doublePage) makeValue(v float64) Value {
+	value := makeValueDouble(v)
+	value.columnIndex = page.columnIndex
+	return value
+}
+
 type byteArrayPage struct {
 	typ         Type
 	values      []byte
@@ -1030,8 +1066,8 @@ func (page *byteArrayPage) bounds() (min, max []byte) {
 func (page *byteArrayPage) Bounds() (min, max Value, ok bool) {
 	if ok = len(page.values) > 0; ok {
 		minBytes, maxBytes := page.bounds()
-		min = makeValueBytes(ByteArray, minBytes)
-		max = makeValueBytes(ByteArray, maxBytes)
+		min = page.makeValueBytes(minBytes)
+		max = page.makeValueBytes(maxBytes)
 	}
 	return min, max, ok
 }
@@ -1077,6 +1113,18 @@ func (page *byteArrayPage) Slice(i, j int64) BufferedPage {
 	}
 }
 
+func (page *byteArrayPage) makeValueBytes(v []byte) Value {
+	value := makeValueBytes(ByteArray, v)
+	value.columnIndex = page.columnIndex
+	return value
+}
+
+func (page *byteArrayPage) makeValueString(v string) Value {
+	value := makeValueString(ByteArray, v)
+	value.columnIndex = page.columnIndex
+	return value
+}
+
 type fixedLenByteArrayPage struct {
 	typ         Type
 	data        []byte
@@ -1141,8 +1189,8 @@ func (page *fixedLenByteArrayPage) bounds() (min, max []byte) {
 func (page *fixedLenByteArrayPage) Bounds() (min, max Value, ok bool) {
 	if ok = len(page.data) > 0; ok {
 		minBytes, maxBytes := page.bounds()
-		min = makeValueBytes(FixedLenByteArray, minBytes)
-		max = makeValueBytes(FixedLenByteArray, maxBytes)
+		min = page.makeValueBytes(minBytes)
+		max = page.makeValueBytes(maxBytes)
 	}
 	return min, max, ok
 }
@@ -1165,6 +1213,18 @@ func (page *fixedLenByteArrayPage) Slice(i, j int64) BufferedPage {
 	}
 }
 
+func (page *fixedLenByteArrayPage) makeValueBytes(v []byte) Value {
+	value := makeValueBytes(FixedLenByteArray, v)
+	value.columnIndex = page.columnIndex
+	return value
+}
+
+func (page *fixedLenByteArrayPage) makeValueString(v string) Value {
+	value := makeValueString(FixedLenByteArray, v)
+	value.columnIndex = page.columnIndex
+	return value
+}
+
 type uint32Page struct {
 	typ         Type
 	values      []uint32
@@ -1212,8 +1272,8 @@ func (page *uint32Page) bounds() (min, max uint32) { return bits.MinMaxUint32(pa
 func (page *uint32Page) Bounds() (min, max Value, ok bool) {
 	if ok = len(page.values) > 0; ok {
 		minUint32, maxUint32 := page.bounds()
-		min = makeValueUint32(minUint32)
-		max = makeValueUint32(maxUint32)
+		min = page.makeValue(minUint32)
+		max = page.makeValue(maxUint32)
 	}
 	return min, max, ok
 }
@@ -1234,6 +1294,12 @@ func (page *uint32Page) Slice(i, j int64) BufferedPage {
 	}
 }
 
+func (page *uint32Page) makeValue(v uint32) Value {
+	value := makeValueUint32(v)
+	value.columnIndex = page.columnIndex
+	return value
+}
+
 type uint64Page struct {
 	typ         Type
 	values      []uint64
@@ -1281,8 +1347,8 @@ func (page *uint64Page) bounds() (min, max uint64) { return bits.MinMaxUint64(pa
 func (page *uint64Page) Bounds() (min, max Value, ok bool) {
 	if ok = len(page.values) > 0; ok {
 		minUint64, maxUint64 := page.bounds()
-		min = makeValueUint64(minUint64)
-		max = makeValueUint64(maxUint64)
+		min = page.makeValue(minUint64)
+		max = page.makeValue(maxUint64)
 	}
 	return min, max, ok
 }
@@ -1303,6 +1369,12 @@ func (page *uint64Page) Slice(i, j int64) BufferedPage {
 	}
 }
 
+func (page *uint64Page) makeValue(v uint64) Value {
+	value := makeValueUint64(v)
+	value.columnIndex = page.columnIndex
+	return value
+}
+
 type nullPage struct {
 	typ    Type
 	column int
diff --git a/parquet_amd64.go b/parquet_amd64.go
index df3f864..2ffbd06 100644
--- a/parquet_amd64.go
+++ b/parquet_amd64.go
@@ -13,7 +13,9 @@ const (
 var (
 	// This variable is used in x86 assembly source files to gate the use of
 	// AVX2 instructions depending on whether the CPU supports it.
-	hasAVX2 = cpu.X86.HasAVX2
+	hasAVX2     = cpu.X86.HasAVX2
+	hasAVX512F  = cpu.X86.HasAVX512F
+	hasAVX512VL = cpu.X86.HasAVX512F && cpu.X86.HasAVX512VL
 )
 
 func optimize(n int) bool {
diff --git a/value.go b/value.go
index 6c0a339..aebd728 100644
--- a/value.go
+++ b/value.go
@@ -763,7 +763,7 @@ func unsafeByteArray(v reflect.Value, n int) []byte {
 // the underlying byte arrays are tested for equality.
 //
 // Note that the repetition levels, definition levels, and column indexes are
-// not compared by this function.
+// not compared by this function, use DeepEqual instead.
 func Equal(v1, v2 Value) bool {
 	if v1.kind != v2.kind {
 		return false
@@ -790,6 +790,17 @@ func Equal(v1, v2 Value) bool {
 	}
 }
 
+// DeepEqual returns true if v1 and v2 are equal, including their repetition
+// levels, definition levels, and column indexes.
+//
+// See Equal for details about how value equality is determined.
+func DeepEqual(v1, v2 Value) bool {
+	return Equal(v1, v2) &&
+		v1.repetitionLevel == v2.repetitionLevel &&
+		v1.definitionLevel == v2.definitionLevel &&
+		v1.columnIndex == v2.columnIndex
+}
+
 var (
 	_ fmt.Formatter = Value{}
 	_ fmt.Stringer  = Value{}
diff --git a/value_amd64.go b/value_amd64.go
new file mode 100644
index 0000000..f1c8d11
--- /dev/null
+++ b/value_amd64.go
@@ -0,0 +1,16 @@
+//go:build !purego
+
+package parquet
+
+//go:noescape
+func memsetValuesAVX2(values []Value, model Value, _ uint64)
+
+func memsetValues(values []Value, model Value) {
+	if hasAVX2 {
+		memsetValuesAVX2(values, model, 0)
+	} else {
+		for i := range values {
+			values[i] = model
+		}
+	}
+}
diff --git a/value_amd64.s b/value_amd64.s
new file mode 100644
index 0000000..2551174
--- /dev/null
+++ b/value_amd64.s
@@ -0,0 +1,59 @@
+//go:build !purego
+
+#include "textflag.h"
+
+#define sizeOfValue 24
+
+// This function is an optimized implementation of the memsetValues function
+// which assigns the parquet.Value passed as second argument to all elements of
+// the first slice argument.
+//
+// The optimizations relies on the fact that we can pack 4 parquet.Value values
+// into 3 YMM registers (24 x 4 = 32 x 3 = 96).
+//
+// func memsetValuesAVX2(values []Value, model Value, _ uint64)
+TEXT ·memsetValuesAVX2(SB), NOSPLIT, $0-56 // 48 + padding to load model in YMM
+    MOVQ values_base+0(FP), AX
+    MOVQ values_len+8(FP), BX
+
+    MOVQ model_ptr+24(FP), R10
+    MOVQ model_u64+32(FP), R11
+    MOVQ model+40(FP), R12 // go vet complains about this line but it's OK
+
+    XORQ SI, SI // byte index
+    MOVQ BX, DI // byte count
+    IMULQ $sizeOfValue, DI
+
+    CMPQ BX, $4
+    JB test
+
+    MOVQ BX, R8
+    SHRQ $2, R8
+    SHLQ $2, R8
+    IMULQ $sizeOfValue, R8
+
+    VMOVDQU model+24(FP), Y0
+    VMOVDQU Y0, Y1
+    VMOVDQU Y0, Y2
+
+    VPERMQ $0b00100100, Y0, Y0
+    VPERMQ $0b01001001, Y1, Y1
+    VPERMQ $0b10010010, Y2, Y2
+loop4:
+    VMOVDQU Y0, 0(AX)(SI*1)
+    VMOVDQU Y1, 32(AX)(SI*1)
+    VMOVDQU Y2, 64(AX)(SI*1)
+    ADDQ $4*sizeOfValue, SI
+    CMPQ SI, R8
+    JNE loop4
+    VZEROUPPER
+    JMP test
+loop:
+    MOVQ R10, 0(AX)(SI*1)
+    MOVQ R11, 8(AX)(SI*1)
+    MOVQ R12, 16(AX)(SI*1)
+    ADDQ $sizeOfValue, SI
+test:
+    CMPQ SI, DI
+    JNE loop
+    RET
diff --git a/values_purego.go b/values_purego.go
new file mode 100644
index 0000000..8151134
--- /dev/null
+++ b/values_purego.go
@@ -0,0 +1,9 @@
+//go:build purego || !amd64
+
+package parquet
+
+func memsetValues(values []Value, model Value) {
+	for i := range values {
+		values[i] = model
+	}
+}


Test patch:
diff --git a/dictionary_test.go b/dictionary_test.go
index ac1e8b4..8a1bd94 100644
--- a/dictionary_test.go
+++ b/dictionary_test.go
@@ -3,47 +3,180 @@ package parquet_test
 import (
 	"math/rand"
 	"testing"
+	"time"
 
 	"github.com/segmentio/parquet-go"
 )
 
-func BenchmarkDictionaryPageBounds(b *testing.B) {
-	benchmarkDictionary(b, func(b *testing.B, typ parquet.Type) {
-		values := make([]parquet.Value, 1e3)
-		f := randValueFuncOf(typ)
-		r := rand.New(rand.NewSource(0))
+var dictionaryTypes = [...]parquet.Type{
+	parquet.BooleanType,
+	parquet.Int32Type,
+	parquet.Int64Type,
+	parquet.Int96Type,
+	parquet.FloatType,
+	parquet.DoubleType,
+	parquet.ByteArrayType,
+	parquet.FixedLenByteArrayType(16),
+	parquet.Uint(32).Type(),
+	parquet.Uint(64).Type(),
+}
+
+func TestDictionary(t *testing.T) {
+	for _, typ := range dictionaryTypes {
+		t.Run(typ.String(), func(t *testing.T) {
+			testDictionary(t, typ)
+		})
+	}
+}
+
+func testDictionary(t *testing.T, typ parquet.Type) {
+	const columnIndex = 1
+	const numValues = 500
+
+	dict := typ.NewDictionary(columnIndex, 0, nil)
+	values := make([]parquet.Value, numValues)
+	indexes := make([]int32, numValues)
+	lookups := make([]parquet.Value, numValues)
 
-		for i := range values {
-			values[i] = f(r)
+	f := randValueFuncOf(typ)
+	r := rand.New(rand.NewSource(0))
+
+	for i := range values {
+		values[i] = f(r)
+		values[i] = values[i].Level(0, 0, columnIndex)
+	}
+
+	mapping := make(map[int32]parquet.Value, numValues)
+
+	for i := 0; i < numValues; {
+		j := i + ((numValues-i)/2 + 1)
+		if j > numValues {
+			j = numValues
 		}
 
-		for i := 0; i < b.N; i++ {
-			page := randDictionaryPage(typ, values)
-			page.Bounds()
+		dict.Insert(indexes[i:j], values[i:j])
+
+		for k, v := range values[i:j] {
+			mapping[indexes[i+k]] = v
+		}
+
+		for _, index := range indexes[i:j] {
+			if index < 0 || index >= int32(dict.Len()) {
+				t.Fatalf("index out of bounds: %d", index)
+			}
+		}
+
+		r.Shuffle(j-i, func(a, b int) {
+			indexes[a+i], indexes[b+i] = indexes[b+i], indexes[a+i]
+		})
+
+		dict.Lookup(indexes[i:j], lookups[i:j])
+
+		for lookupIndex, valueIndex := range indexes[i:j] {
+			want := mapping[valueIndex]
+			got := lookups[lookupIndex+i]
+
+			if !parquet.DeepEqual(want, got) {
+				t.Fatalf("wrong value looked up at index %d: want=%#v got=%#v", valueIndex, want, got)
+			}
+		}
+
+		minValue := values[i]
+		maxValue := values[i]
+
+		for _, value := range values[i+1 : j] {
+			switch {
+			case typ.Compare(value, minValue) < 0:
+				minValue = value
+			case typ.Compare(value, maxValue) > 0:
+				maxValue = value
+			}
+		}
+
+		lowerBound, upperBound := dict.Bounds(indexes[i:j])
+		if !parquet.DeepEqual(lowerBound, minValue) {
+			t.Errorf("wrong lower bound betwen indexes %d and %d: want=%#v got=%#v", i, j, minValue, lowerBound)
+		}
+		if !parquet.DeepEqual(upperBound, maxValue) {
+			t.Errorf("wrong upper bound between indexes %d and %d: want=%#v got=%#v", i, j, maxValue, upperBound)
 		}
-	})
-}
 
-func benchmarkDictionary(b *testing.B, do func(*testing.B, parquet.Type)) {
-	types := []parquet.Type{
-		parquet.BooleanType,
-		parquet.Int32Type,
-		parquet.Int64Type,
-		parquet.Int96Type,
-		parquet.FloatType,
-		parquet.DoubleType,
-		parquet.ByteArrayType,
+		i = j
 	}
 
-	for _, typ := range types {
-		b.Run(typ.String(), func(b *testing.B) { do(b, typ) })
+	for i := range lookups {
+		lookups[i] = parquet.Value{}
+	}
+
+	dict.Lookup(indexes, lookups)
+
+	for lookupIndex, valueIndex := range indexes {
+		want := mapping[valueIndex]
+		got := lookups[lookupIndex]
+
+		if !parquet.Equal(want, got) {
+			t.Fatalf("wrong value looked up at index %d: want=%+v got=%+v", valueIndex, want, got)
+		}
 	}
 }
 
-func randDictionaryPage(typ parquet.Type, values []parquet.Value) parquet.BufferedPage {
-	const bufferSize = 64 * 1024
-	dict := typ.NewDictionary(0, 0, make([]byte, 0, 4*bufferSize))
-	buf := dict.Type().NewColumnBuffer(0, bufferSize)
-	buf.WriteValues(values)
-	return buf.Page()
+func BenchmarkDictionary(b *testing.B) {
+	const numValues = 1000
+
+	tests := []struct {
+		scenario string
+		init     func(parquet.Dictionary, []int32, []parquet.Value)
+		test     func(parquet.Dictionary, []int32, []parquet.Value)
+	}{
+		{
+			scenario: "Bounds",
+			init:     parquet.Dictionary.Insert,
+			test: func(dict parquet.Dictionary, indexes []int32, _ []parquet.Value) {
+				dict.Bounds(indexes)
+			},
+		},
+
+		{
+			scenario: "Insert",
+			test:     parquet.Dictionary.Insert,
+		},
+
+		{
+			scenario: "Lookup",
+			init:     parquet.Dictionary.Insert,
+			test:     parquet.Dictionary.Lookup,
+		},
+	}
+
+	for _, test := range tests {
+		b.Run(test.scenario, func(b *testing.B) {
+			for _, typ := range dictionaryTypes {
+				dict := typ.NewDictionary(0, 0, make([]byte, 0, 4*numValues))
+				values := make([]parquet.Value, numValues)
+
+				f := randValueFuncOf(typ)
+				r := rand.New(rand.NewSource(0))
+
+				for i := range values {
+					values[i] = f(r)
+				}
+
+				indexes := make([]int32, len(values))
+				if test.init != nil {
+					test.init(dict, indexes, values)
+				}
+
+				b.Run(typ.String(), func(b *testing.B) {
+					start := time.Now()
+
+					for i := 0; i < b.N; i++ {
+						test.test(dict, indexes, values)
+					}
+
+					seconds := time.Since(start).Seconds()
+					b.ReportMetric(float64(numValues*b.N)/seconds, "value/s")
+				})
+			}
+		})
+	}
 }
diff --git a/parquet_test.go b/parquet_test.go
index 5ffd6b9..c0a977b 100644
--- a/parquet_test.go
+++ b/parquet_test.go
@@ -413,9 +413,23 @@ func randValueFuncOf(t parquet.Type) func(*rand.Rand) parquet.Value {
 		return func(r *rand.Rand) parquet.Value {
 			n := r.Intn(49) + 1
 			b := make([]byte, n)
+			const characters = "1234567890qwertyuiopasdfghjklzxcvbnm "
+			for i := range b {
+				b[i] = characters[r.Intn(len(characters))]
+			}
 			return parquet.ValueOf(b)
 		}
 
+	case parquet.FixedLenByteArray:
+		arrayType := reflect.ArrayOf(t.Length(), reflect.TypeOf(byte(0)))
+		return func(r *rand.Rand) parquet.Value {
+			b := make([]byte, arrayType.Len())
+			r.Read(b)
+			v := reflect.New(arrayType).Elem()
+			reflect.Copy(v, reflect.ValueOf(b))
+			return parquet.ValueOf(v.Interface())
+		}
+
 	default:
 		panic("NOT IMPLEMENTED")
 	}
diff --git a/value_test.go b/value_test.go
index 3fcf5b4..cf2fa05 100644
--- a/value_test.go
+++ b/value_test.go
@@ -74,7 +74,7 @@ func TestValueClone(t *testing.T) {
 				v := parquet.ValueOf(value)
 				c := v.Clone()
 
-				if !parquet.Equal(v, c) {
+				if !parquet.DeepEqual(v, c) {
 					t.Errorf("cloned values are not equal: want=%#v got=%#v", v, c)
 				}
 				if v.RepetitionLevel() != c.RepetitionLevel() {