Repository: segmentio/parquet-go
Pull request #186 (2022-05-21 00:07:23)

Add parquet.GenericBuffer, parquet.GenericReader, parquet.GenericWriter
This PR adds new generic APIs for programs compiling with Go 1.18+, which are equivalents to the existing `parquet.Buffer`, `parquet.Reader`, and `parquet.Writer`, but type-parameterized to have compile-time guarantees of the row types that are read or written.

These new APIs create many opportunities to create both safer and more efficient programs, which I tried to document in the source code.

I added benchmarks to compare the performance of the new APIs and worked on optimizing the writes.

```
goos: linux
goarch: amd64
pkg: github.com/segmentio/parquet-go
cpu: Intel(R) Xeon(R) CPU @ 2.60GHz
```

```
name           old time/op  new time/op   delta
GenericBuffer  15.6µs ± 2%    0.3µs ± 1%    -97.78%  (p=0.000 n=10+10)

name           old row/s    new row/s     delta
GenericBuffer   6.39M ± 2%  287.75M ± 1%  +4401.54%  (p=0.000 n=10+10)
```

```
name           old time/op  new time/op  delta
GenericWriter  17.6µs ± 2%   1.0µs ± 2%    -94.06%  (p=0.000 n=9+10)

name           old row/s    new row/s    delta
GenericWriter   5.68M ± 2%  95.75M ± 1%  +1587.10%  (p=0.000 n=9+9)
```

```
name           old time/op  new time/op  delta
GenericReader  9.87µs ± 3%  7.33µs ± 4%  -25.79%  (p=0.000 n=9+10)

name           old row/s    new row/s    delta
GenericReader   10.1M ± 3%   13.5M ± 4%  +33.53%  (p=0.000 n=9+10)
```

I will leave further optimization of the `parquet.Reader` type to a follow up.


Patch:
diff --git a/buffer.go b/buffer.go
index a7441dd..472e67d 100644
--- a/buffer.go
+++ b/buffer.go
@@ -12,7 +12,7 @@ import (
 type Buffer struct {
 	config  *RowGroupConfig
 	schema  *Schema
-	rowbuf  []Value
+	rowbuf  []Row
 	colbuf  [][]Value
 	chunks  []ColumnChunk
 	columns []ColumnBuffer
@@ -93,7 +93,7 @@ func (buf *Buffer) configure(schema *Schema) {
 	})
 
 	buf.schema = schema
-	buf.rowbuf = make([]Value, 0, 10)
+	buf.rowbuf = make([]Row, 0, 1)
 	buf.colbuf = make([][]Value, len(buf.columns))
 	buf.chunks = make([]ColumnChunk, len(buf.columns))
 
@@ -183,11 +183,12 @@ func (buf *Buffer) Write(row interface{}) error {
 	if buf.schema == nil {
 		buf.configure(SchemaOf(row))
 	}
-	defer func() {
-		clearValues(buf.rowbuf)
-	}()
-	buf.rowbuf = buf.schema.Deconstruct(buf.rowbuf[:0], row)
-	_, err := buf.WriteRows([]Row{buf.rowbuf})
+
+	buf.rowbuf = buf.rowbuf[:1]
+	defer clearRows(buf.rowbuf)
+
+	buf.rowbuf[0] = buf.schema.Deconstruct(buf.rowbuf[0], row)
+	_, err := buf.WriteRows(buf.rowbuf)
 	return err
 }
 
diff --git a/buffer_go18.go b/buffer_go18.go
new file mode 100644
index 0000000..6bc959f
--- /dev/null
+++ b/buffer_go18.go
@@ -0,0 +1,176 @@
+//go:build go1.18
+
+package parquet
+
+import (
+	"reflect"
+	"sort"
+)
+
+// GenericBuffer is similar to a Buffer but uses a type parameter to define the
+// Go type representing the schema of rows in the buffer.
+//
+// See GenericWriter for details about the benefits over the classic Buffer API.
+type GenericBuffer[T any] struct {
+	base    Buffer
+	write   bufferFunc[T]
+	columns columnBufferWriter
+}
+
+// NewGenericBuffer is like NewBuffer but returns a GenericBuffer[T] suited to write
+// rows of Go type T.
+//
+// The type parameter T should be a map, struct, or any. Any other types will
+// cause a panic at runtime. Type checking is a lot more effective when the
+// generic parameter is a struct type, using map and interface types is somewhat
+// similar to using a Writer.
+//
+// If the option list may explicitly declare a schema, it must be compatible
+// with the schema generated from T.
+func NewGenericBuffer[T any](options ...RowGroupOption) *GenericBuffer[T] {
+	config, err := NewRowGroupConfig(options...)
+	if err != nil {
+		panic(err)
+	}
+
+	t := typeOf[T]()
+	if config.Schema == nil {
+		config.Schema = schemaOf(dereference(t))
+	}
+
+	buf := &GenericBuffer[T]{
+		base: Buffer{config: config},
+	}
+	buf.base.configure(config.Schema)
+	buf.write = bufferFuncOf[T](t, config.Schema)
+	buf.columns = columnBufferWriter{
+		columns: buf.base.columns,
+		values:  make([]Value, 0, defaultValueBufferSize),
+	}
+	return buf
+}
+
+func typeOf[T any]() reflect.Type {
+	var v T
+	return reflect.TypeOf(v)
+}
+
+type bufferFunc[T any] func(*GenericBuffer[T], []T) (int, error)
+
+func bufferFuncOf[T any](t reflect.Type, schema *Schema) bufferFunc[T] {
+	switch t.Kind() {
+	case reflect.Interface, reflect.Map:
+		return (*GenericBuffer[T]).writeRows
+
+	case reflect.Struct:
+		return makeBufferFunc[T](t, schema)
+
+	case reflect.Pointer:
+		if e := t.Elem(); e.Kind() == reflect.Struct {
+			return makeBufferFunc[T](t, schema)
+		}
+	}
+	panic("cannot create buffer for values of type " + t.String())
+}
+
+func makeBufferFunc[T any](t reflect.Type, schema *Schema) bufferFunc[T] {
+	size := t.Size()
+	writeRows := writeRowsFuncOf(t, schema, nil)
+	return func(buf *GenericBuffer[T], rows []T) (n int, err error) {
+		defer buf.columns.clear()
+		err = writeRows(&buf.columns, makeArray(rows), size, 0, columnLevels{})
+		if err == nil {
+			n = len(rows)
+		}
+		return n, err
+	}
+}
+
+func (buf *GenericBuffer[T]) Size() int64 {
+	return buf.base.Size()
+}
+
+func (buf *GenericBuffer[T]) NumRows() int64 {
+	return buf.base.NumRows()
+}
+
+func (buf *GenericBuffer[T]) ColumnChunks() []ColumnChunk {
+	return buf.base.ColumnChunks()
+}
+
+func (buf *GenericBuffer[T]) ColumnBuffers() []ColumnBuffer {
+	return buf.base.ColumnBuffers()
+}
+
+func (buf *GenericBuffer[T]) SortingColumns() []SortingColumn {
+	return buf.base.SortingColumns()
+}
+
+func (buf *GenericBuffer[T]) Len() int {
+	return buf.base.Len()
+}
+
+func (buf *GenericBuffer[T]) Less(i, j int) bool {
+	return buf.base.Less(i, j)
+}
+
+func (buf *GenericBuffer[T]) Swap(i, j int) {
+	buf.base.Swap(i, j)
+}
+
+func (buf *GenericBuffer[T]) Reset() {
+	buf.base.Reset()
+}
+
+func (buf *GenericBuffer[T]) Write(rows []T) (int, error) {
+	if len(rows) == 0 {
+		return 0, nil
+	}
+	return buf.write(buf, rows)
+}
+
+func (buf *GenericBuffer[T]) WriteRows(rows []Row) (int, error) {
+	return buf.base.WriteRows(rows)
+}
+
+func (buf *GenericBuffer[T]) WriteRowGroup(rowGroup RowGroup) (int64, error) {
+	return buf.base.WriteRowGroup(rowGroup)
+}
+
+func (buf *GenericBuffer[T]) Rows() Rows {
+	return buf.base.Rows()
+}
+
+func (buf *GenericBuffer[T]) Schema() *Schema {
+	return buf.base.Schema()
+}
+
+func (buf *GenericBuffer[T]) writeRows(rows []T) (int, error) {
+	if cap(buf.base.rowbuf) < len(rows) {
+		buf.base.rowbuf = make([]Row, len(rows))
+	} else {
+		buf.base.rowbuf = buf.base.rowbuf[:len(rows)]
+	}
+	defer clearRows(buf.base.rowbuf)
+
+	schema := buf.base.Schema()
+	for i := range rows {
+		buf.base.rowbuf[i] = schema.Deconstruct(buf.base.rowbuf[i], &rows[i])
+	}
+
+	return buf.base.WriteRows(buf.base.rowbuf)
+}
+
+var (
+	_ RowGroup       = (*GenericBuffer[any])(nil)
+	_ RowGroupWriter = (*GenericBuffer[any])(nil)
+	_ sort.Interface = (*GenericBuffer[any])(nil)
+
+	_ RowGroup       = (*GenericBuffer[struct{}])(nil)
+	_ RowGroupWriter = (*GenericBuffer[struct{}])(nil)
+	_ sort.Interface = (*GenericBuffer[struct{}])(nil)
+
+	_ RowGroup       = (*GenericBuffer[map[struct{}]struct{}])(nil)
+	_ RowGroupWriter = (*GenericBuffer[map[struct{}]struct{}])(nil)
+	_ sort.Interface = (*GenericBuffer[map[struct{}]struct{}])(nil)
+)
diff --git a/column_buffer.go b/column_buffer.go
index 27d1f22..a5a3542 100644
--- a/column_buffer.go
+++ b/column_buffer.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"io"
 	"sort"
+	"unsafe"
 
 	"github.com/segmentio/parquet-go/deprecated"
 	"github.com/segmentio/parquet-go/encoding/plain"
@@ -59,6 +60,15 @@ type ColumnBuffer interface {
 	Size() int64
 }
 
+type array struct {
+	ptr unsafe.Pointer
+	len int
+}
+
+func (a array) index(i int, size, offset uintptr) unsafe.Pointer {
+	return unsafe.Add(a.ptr, uintptr(i)*size+offset)
+}
+
 func columnIndexOfNullable(base ColumnBuffer, maxDefinitionLevel byte, definitionLevels []byte) ColumnIndex {
 	return &nullableColumnIndex{
 		ColumnIndex:        base.ColumnIndex(),
@@ -751,20 +761,88 @@ func (col *booleanColumnBuffer) Swap(i, j int) {
 }
 
 func (col *booleanColumnBuffer) WriteBooleans(values []bool) (int, error) {
-	col.writeValues(len(values), func(i int) bool { return values[i] })
+	var rows = array{
+		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&values)),
+		len: len(values),
+	}
+	col.writeValues(rows, unsafe.Sizeof(false), 0)
 	return len(values), nil
 }
 
 func (col *booleanColumnBuffer) WriteValues(values []Value) (int, error) {
-	col.writeValues(len(values), func(i int) bool { return values[i].Boolean() })
+	var rows = array{
+		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&values)),
+		len: len(values),
+	}
+	var value Value
+	col.writeValues(rows, unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 	return len(values), nil
 }
 
-func (col *booleanColumnBuffer) writeValues(n int, f func(int) bool) {
-	for i := 0; i < n; i++ {
-		col.bits = plain.AppendBoolean(col.bits, int(col.numValues), f(i))
+func (col *booleanColumnBuffer) writeValues(rows array, size, offset uintptr) {
+	numBytes := bits.ByteCount(uint(col.numValues) + uint(rows.len))
+	if cap(col.bits) < numBytes {
+		col.bits = append(make([]byte, 0, 2*cap(col.bits)), col.bits...)
+	}
+	col.bits = col.bits[:numBytes]
+	i := 0
+	r := 8 - (int(col.numValues) % 8)
+
+	if r <= rows.len {
+		// First we attempt to write enough bits to align the number of values
+		// in the column buffer on 8 bytes. After this step the next bit should
+		// be written at the zero'th index of a byte of the buffer.
+		var b byte
+		for i < r {
+			v := *(*byte)(rows.index(i, size, offset))
+			b |= (v & 1) << uint(i)
+			i++
+		}
+		x := uint(col.numValues) / 8
+		y := uint(col.numValues) % 8
+		col.bits[x] |= (b << y) | (col.bits[x] & ^(0xFF << y))
+		col.numValues += int32(i)
+
+		if n := rows.len - i; n >= 8 {
+			// At this stage, we know that that we have at least 8 bits to write
+			// and the bits will be aligned on the address of a byte in the
+			// output buffer. We can work on 8 values per loop iteration,
+			// packing them into a single byte and writing it to the output
+			// buffer. This effectively reduces by 87.5% the number of memory
+			// stores that the program needs to perform to generate the values.
+			for j := i + (n/8)*8; i < j; i += 8 {
+				b0 := *(*byte)(rows.index(i+0, size, offset))
+				b1 := *(*byte)(rows.index(i+1, size, offset))
+				b2 := *(*byte)(rows.index(i+2, size, offset))
+				b3 := *(*byte)(rows.index(i+3, size, offset))
+				b4 := *(*byte)(rows.index(i+4, size, offset))
+				b5 := *(*byte)(rows.index(i+5, size, offset))
+				b6 := *(*byte)(rows.index(i+6, size, offset))
+				b7 := *(*byte)(rows.index(i+7, size, offset))
+
+				col.bits[col.numValues/8] = (b0 & 1) |
+					((b1 & 1) << 1) |
+					((b2 & 1) << 2) |
+					((b3 & 1) << 3) |
+					((b4 & 1) << 4) |
+					((b5 & 1) << 5) |
+					((b6 & 1) << 6) |
+					((b7 & 1) << 7)
+				col.numValues += 8
+			}
+		}
+	}
+
+	for i < rows.len {
+		x := uint(col.numValues) / 8
+		y := uint(col.numValues) % 8
+		b := *(*byte)(rows.index(i, size, offset))
+		col.bits[x] = ((b & 1) << y) | (col.bits[x] & ^(1 << y))
 		col.numValues++
+		i++
 	}
+
+	col.bits = col.bits[:bits.ByteCount(uint(col.numValues))]
 }
 
 func (col *booleanColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -848,12 +926,29 @@ func (col *int32ColumnBuffer) WriteInt32s(values []int32) (int, error) {
 }
 
 func (col *int32ColumnBuffer) WriteValues(values []Value) (int, error) {
-	for _, v := range values {
-		col.values = append(col.values, v.Int32())
+	var rows = array{
+		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&values)),
+		len: len(values),
 	}
+	var value Value
+	col.writeValues(rows, unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 	return len(values), nil
 }
 
+func (col *int32ColumnBuffer) writeValues(rows array, size, offset uintptr) {
+	if n := len(col.values) + rows.len; n > cap(col.values) {
+		col.values = append(make([]int32, 0, max(n, 2*cap(col.values))), col.values...)
+	}
+
+	n := len(col.values)
+	col.values = col.values[:n+rows.len]
+
+	values := col.values[n:]
+	for i := range values {
+		values[i] = *(*int32)(rows.index(i, size, offset))
+	}
+}
+
 func (col *int32ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
 	i := int(offset)
 	switch {
@@ -935,12 +1030,29 @@ func (col *int64ColumnBuffer) WriteInt64s(values []int64) (int, error) {
 }
 
 func (col *int64ColumnBuffer) WriteValues(values []Value) (int, error) {
-	for _, v := range values {
-		col.values = append(col.values, v.Int64())
+	var rows = array{
+		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&values)),
+		len: len(values),
 	}
+	var value Value
+	col.writeValues(rows, unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 	return len(values), nil
 }
 
+func (col *int64ColumnBuffer) writeValues(rows array, size, offset uintptr) {
+	if n := len(col.values) + rows.len; n > cap(col.values) {
+		col.values = append(make([]int64, 0, max(n, 2*cap(col.values))), col.values...)
+	}
+
+	n := len(col.values)
+	col.values = col.values[:n+rows.len]
+
+	values := col.values[n:]
+	for i := range values {
+		values[i] = *(*int64)(rows.index(i, size, offset))
+	}
+}
+
 func (col *int64ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
 	i := int(offset)
 	switch {
@@ -1028,6 +1140,13 @@ func (col *int96ColumnBuffer) WriteValues(values []Value) (int, error) {
 	return len(values), nil
 }
 
+func (col *int96ColumnBuffer) writeValues(rows array, size, offset uintptr) {
+	for i := 0; i < rows.len; i++ {
+		p := rows.index(i, size, offset)
+		col.values = append(col.values, *(*deprecated.Int96)(p))
+	}
+}
+
 func (col *int96ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
 	i := int(offset)
 	switch {
@@ -1109,12 +1228,29 @@ func (col *floatColumnBuffer) WriteFloats(values []float32) (int, error) {
 }
 
 func (col *floatColumnBuffer) WriteValues(values []Value) (int, error) {
-	for _, v := range values {
-		col.values = append(col.values, v.Float())
+	var rows = array{
+		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&values)),
+		len: len(values),
 	}
+	var value Value
+	col.writeValues(rows, unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 	return len(values), nil
 }
 
+func (col *floatColumnBuffer) writeValues(rows array, size, offset uintptr) {
+	if n := len(col.values) + rows.len; n > cap(col.values) {
+		col.values = append(make([]float32, 0, max(n, 2*cap(col.values))), col.values...)
+	}
+
+	n := len(col.values)
+	col.values = col.values[:n+rows.len]
+
+	values := col.values[n:]
+	for i := range values {
+		values[i] = *(*float32)(rows.index(i, size, offset))
+	}
+}
+
 func (col *floatColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
 	i := int(offset)
 	switch {
@@ -1196,12 +1332,29 @@ func (col *doubleColumnBuffer) WriteDoubles(values []float64) (int, error) {
 }
 
 func (col *doubleColumnBuffer) WriteValues(values []Value) (int, error) {
-	for _, v := range values {
-		col.values = append(col.values, v.Double())
+	var rows = array{
+		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&values)),
+		len: len(values),
 	}
+	var value Value
+	col.writeValues(rows, unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 	return len(values), nil
 }
 
+func (col *doubleColumnBuffer) writeValues(rows array, size, offset uintptr) {
+	if n := len(col.values) + rows.len; n > cap(col.values) {
+		col.values = append(make([]float64, 0, max(n, 2*cap(col.values))), col.values...)
+	}
+
+	n := len(col.values)
+	col.values = col.values[:n+rows.len]
+
+	values := col.values[n:]
+	for i := range values {
+		values[i] = *(*float64)(rows.index(i, size, offset))
+	}
+}
+
 func (col *doubleColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
 	i := int(offset)
 	switch {
@@ -1317,10 +1470,6 @@ func (col *byteArrayColumnBuffer) Write(b []byte) (int, error) {
 	return n, err
 }
 
-func (col *byteArrayColumnBuffer) WriteRequired(values []byte) (int, error) {
-	return col.WriteByteArrays(values)
-}
-
 func (col *byteArrayColumnBuffer) WriteByteArrays(values []byte) (int, error) {
 	n, _, err := col.writeByteArrays(values)
 	return n, err
@@ -1330,7 +1479,7 @@ func (col *byteArrayColumnBuffer) writeByteArrays(values []byte) (count, bytes i
 	baseCount, baseBytes := len(col.offsets), len(col.values)
 
 	err = plain.RangeByteArrays(values, func(value []byte) error {
-		col.append(value)
+		col.append(bits.BytesToString(value))
 		return nil
 	})
 
@@ -1338,12 +1487,22 @@ func (col *byteArrayColumnBuffer) writeByteArrays(values []byte) (count, bytes i
 }
 
 func (col *byteArrayColumnBuffer) WriteValues(values []Value) (int, error) {
-	for _, value := range values {
-		col.append(value.ByteArray())
+	rows := array{
+		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&values)),
+		len: len(values),
 	}
+	var value Value
+	col.writeValues(rows, unsafe.Sizeof(value), unsafe.Offsetof(value.ptr))
 	return len(values), nil
 }
 
+func (col *byteArrayColumnBuffer) writeValues(rows array, size, offset uintptr) {
+	for i := 0; i < rows.len; i++ {
+		p := rows.index(i, size, offset)
+		col.append(*(*string)(p))
+	}
+}
+
 func (col *byteArrayColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
 	i := int(offset)
 	switch {
@@ -1365,10 +1524,9 @@ func (col *byteArrayColumnBuffer) ReadValuesAt(values []Value, offset int64) (n
 	}
 }
 
-func (col *byteArrayColumnBuffer) append(value []byte) {
-	offset := uint32(len(col.values))
-	col.values = plain.AppendByteArray(col.values, value)
-	col.offsets = append(col.offsets, offset)
+func (col *byteArrayColumnBuffer) append(value string) {
+	col.offsets = append(col.offsets, uint32(len(col.values)))
+	col.values = plain.AppendByteArrayString(col.values, value)
 	col.numValues++
 }
 
@@ -1446,10 +1604,6 @@ func (col *fixedLenByteArrayColumnBuffer) Write(b []byte) (int, error) {
 	return n * col.size, err
 }
 
-func (col *fixedLenByteArrayColumnBuffer) WriteRequired(values []byte) (int, error) {
-	return col.WriteFixedLenByteArrays(values)
-}
-
 func (col *fixedLenByteArrayColumnBuffer) WriteFixedLenByteArrays(values []byte) (int, error) {
 	d, m := len(values)/col.size, len(values)%col.size
 	if m != 0 {
@@ -1466,6 +1620,30 @@ func (col *fixedLenByteArrayColumnBuffer) WriteValues(values []Value) (int, erro
 	return len(values), nil
 }
 
+func (col *fixedLenByteArrayColumnBuffer) writeValues(rows array, size, offset uintptr) {
+	for i := 0; i < rows.len; i++ {
+		p := rows.index(i, size, offset)
+		col.data = append(col.data, unsafe.Slice((*byte)(p), col.size)...)
+	}
+}
+
+func (col *fixedLenByteArrayColumnBuffer) writeValues128(rows array, size, offset uintptr) {
+	c := cap(col.data)
+	n := len(col.data) + (16 * rows.len)
+	if c < n {
+		col.data = append(make([]byte, 0, max(n, 2*c)), col.data...)
+	}
+
+	firstIndex := len(col.data)
+	col.data = col.data[:len(col.data)+(16*rows.len)]
+
+	data := unsafe.Pointer(&col.data[firstIndex])
+	for i := 0; i < rows.len; i++ {
+		p := rows.index(i, size, offset)
+		*(*[16]byte)(unsafe.Add(data, i*16)) = *(*[16]byte)(p)
+	}
+}
+
 func (col *fixedLenByteArrayColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
 	i := int(offset) * col.size
 	switch {
@@ -1547,12 +1725,29 @@ func (col *uint32ColumnBuffer) WriteUint32s(values []uint32) (int, error) {
 }
 
 func (col *uint32ColumnBuffer) WriteValues(values []Value) (int, error) {
-	for _, v := range values {
-		col.values = append(col.values, v.Uint32())
+	var rows = array{
+		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&values)),
+		len: len(values),
 	}
+	var value Value
+	col.writeValues(rows, unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 	return len(values), nil
 }
 
+func (col *uint32ColumnBuffer) writeValues(rows array, size, offset uintptr) {
+	if n := len(col.values) + rows.len; n > cap(col.values) {
+		col.values = append(make([]uint32, 0, max(n, 2*cap(col.values))), col.values...)
+	}
+
+	n := len(col.values)
+	col.values = col.values[:n+rows.len]
+
+	values := col.values[n:]
+	for i := range values {
+		values[i] = *(*uint32)(rows.index(i, size, offset))
+	}
+}
+
 func (col *uint32ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
 	i := int(offset)
 	switch {
@@ -1634,12 +1829,29 @@ func (col *uint64ColumnBuffer) WriteUint64s(values []uint64) (int, error) {
 }
 
 func (col *uint64ColumnBuffer) WriteValues(values []Value) (int, error) {
-	for _, v := range values {
-		col.values = append(col.values, v.Uint64())
+	var rows = array{
+		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&values)),
+		len: len(values),
 	}
+	var value Value
+	col.writeValues(rows, unsafe.Sizeof(value), unsafe.Offsetof(value.u64))
 	return len(values), nil
 }
 
+func (col *uint64ColumnBuffer) writeValues(rows array, size, offset uintptr) {
+	if n := len(col.values) + rows.len; n > cap(col.values) {
+		col.values = append(make([]uint64, 0, max(n, 2*cap(col.values))), col.values...)
+	}
+
+	n := len(col.values)
+	col.values = col.values[:n+rows.len]
+
+	values := col.values[n:]
+	for i := range values {
+		values[i] = *(*uint64)(rows.index(i, size, offset))
+	}
+}
+
 func (col *uint64ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
 	i := int(offset)
 	switch {
diff --git a/column_buffer_go18.go b/column_buffer_go18.go
new file mode 100644
index 0000000..6cbe9f8
--- /dev/null
+++ b/column_buffer_go18.go
@@ -0,0 +1,912 @@
+//go:build go1.18
+
+package parquet
+
+import (
+	"bytes"
+	"reflect"
+	"unsafe"
+
+	"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)
+}
+
+// nullIndexFunc is the type of functions generated by calling nullIndexFuncOf.
+//
+// The function takes an array of values that it assumes the type of (based on
+// the reflect.Type that it was created from) and returns the index of the first
+// null value, or the length of the array if no values were null.
+//
+// A value is null if it is zero for number types, or if it is nil for a slice
+// type. Struct values are never null.
+type nullIndexFunc func(array) int
+
+func nullIndexByte(a array) int {
+	i := bytes.IndexByte(makeSlice[byte](a), 0)
+	if i < 0 {
+		i = a.len
+	}
+	return i
+}
+
+func nullIndexSlice(a array) int {
+	const size = unsafe.Sizeof(([]byte)(nil))
+	for i := 0; i < a.len; i++ {
+		p := *(*unsafe.Pointer)(a.index(i, size, 0))
+		if p == nil {
+			return i
+		}
+	}
+	return a.len
+}
+
+func nullIndex[T comparable](a array) int {
+	var zero T
+	for i, v := range makeSlice[T](a) {
+		if v == zero {
+			return i
+		}
+	}
+	return a.len
+}
+
+func nullIndexFuncOf(t reflect.Type) nullIndexFunc {
+	switch t {
+	case reflect.TypeOf(deprecated.Int96{}):
+		return nullIndex[deprecated.Int96]
+	}
+
+	switch t.Kind() {
+	case reflect.Bool:
+		return nullIndexByte
+
+	case reflect.Int, reflect.Uint:
+		return nullIndex[int]
+
+	case reflect.Int8, reflect.Uint8:
+		return nullIndexByte
+
+	case reflect.Int16, reflect.Uint16:
+		return nullIndex[int16]
+
+	case reflect.Int32, reflect.Uint32:
+		return nullIndex[int32]
+
+	case reflect.Int64, reflect.Uint64:
+		return nullIndex[int64]
+
+	case reflect.Float32:
+		return nullIndex[float32]
+
+	case reflect.Float64:
+		return nullIndex[float64]
+
+	case reflect.String:
+		return nullIndex[string]
+
+	case reflect.Slice:
+		return nullIndexSlice
+
+	case reflect.Array:
+		if t.Elem().Kind() == reflect.Uint8 {
+			return nullIndexFuncOfArray(t)
+		}
+
+	case reflect.Pointer:
+		return nullIndex[unsafe.Pointer]
+
+	case reflect.Struct:
+		return func(a array) int { return a.len }
+	}
+
+	panic("cannot convert Go values of type " + t.String() + " to parquet value")
+}
+
+func nullIndexFuncOfArray(t reflect.Type) nullIndexFunc {
+	arrayLen := t.Len()
+	return func(a array) int {
+		for i := 0; i < a.len; i++ {
+			p := a.index(i, uintptr(arrayLen), 0)
+			b := slice[byte](p, arrayLen)
+			if bytes.Count(b, []byte{0}) == len(b) {
+				return i
+			}
+		}
+		return a.len
+	}
+}
+
+// nonNullIndexFunc is the type of functions generated by calling
+// nonNullIndexFuncOf.
+//
+// The function takes an array of values that it assumes the type of (based on
+// the reflect.Type that it was created from) and returns the index of the first
+// non-null value, or the length of the array if all values were null.
+//
+// A value is null if it is zero for number types, or if it is nil for a slice
+// type. Struct values are never null.
+type nonNullIndexFunc func(array) int
+
+func nonNullIndexSlice(a array) int {
+	const size = unsafe.Sizeof(([]byte)(nil))
+	for i := 0; i < a.len; i++ {
+		p := *(*unsafe.Pointer)(a.index(i, size, 0))
+		if p != nil {
+			return i
+		}
+	}
+	return a.len
+}
+
+func nonNullIndex[T comparable](a array) int {
+	var zero T
+	for i, v := range makeSlice[T](a) {
+		if v != zero {
+			return i
+		}
+	}
+	return a.len
+}
+
+func nonNullIndexFuncOf(t reflect.Type) nonNullIndexFunc {
+	switch t {
+	case reflect.TypeOf(deprecated.Int96{}):
+		return nonNullIndex[deprecated.Int96]
+	}
+
+	switch t.Kind() {
+	case reflect.Bool:
+		return nonNullIndex[bool]
+
+	case reflect.Int, reflect.Uint:
+		return nonNullIndex[int]
+
+	case reflect.Int8, reflect.Uint8:
+		return nonNullIndex[int8]
+
+	case reflect.Int16, reflect.Uint16:
+		return nonNullIndex[int16]
+
+	case reflect.Int32, reflect.Uint32:
+		return nonNullIndex[int32]
+
+	case reflect.Int64, reflect.Uint64:
+		return nonNullIndex[int64]
+
+	case reflect.Float32:
+		return nonNullIndex[float32]
+
+	case reflect.Float64:
+		return nonNullIndex[float64]
+
+	case reflect.String:
+		return nonNullIndex[string]
+
+	case reflect.Slice:
+		return nonNullIndexSlice
+
+	case reflect.Array:
+		if t.Elem().Kind() == reflect.Uint8 {
+			return nonNullIndexFuncOfArray(t)
+		}
+
+	case reflect.Pointer:
+		return nonNullIndex[unsafe.Pointer]
+
+	case reflect.Struct:
+		return func(array) int { return 0 }
+	}
+
+	panic("cannot convert Go values of type " + t.String() + " to parquet value")
+}
+
+func nonNullIndexFuncOfArray(t reflect.Type) nonNullIndexFunc {
+	arrayLen := t.Len()
+	return func(a array) int {
+		for i := 0; i < a.len; i++ {
+			p := a.index(i, uintptr(arrayLen), 0)
+			b := slice[byte](p, arrayLen)
+			if bytes.Count(b, []byte{0}) != len(b) {
+				return i
+			}
+		}
+		return a.len
+	}
+}
+
+type columnLevels struct {
+	columnIndex     int16
+	repetitionDepth byte
+	repetitionLevel byte
+	definitionLevel byte
+}
+
+// columnBufferWriter is a type used to writes rows to column buffers.
+type columnBufferWriter struct {
+	columns []ColumnBuffer // column buffers where row values are written
+	values  []Value        // buffer used to temporarily hold values
+	maxLen  int            // max number of values written to the temp buffer
+}
+
+// writeRowsFunc is the type of functions that apply rows to a set of column
+// buffers.
+//
+// - w is the columnBufferWriter holding the column buffers where the rows are
+//   written.
+//
+// - rows is the array of Go values to write to the column buffers.
+//
+// - size is the size of Go values in the rows array (in bytes).
+//
+// - offset is the byte offset of the value being written in each element of the
+//   rows array.
+//
+// - levels is used to track the column index, repetition and definition levels
+//   of values when writing optional or repeated columns.
+//
+type writeRowsFunc func(w *columnBufferWriter, rows array, size, offset uintptr, levels columnLevels) error
+
+// writeRowsFuncOf generates a writeRowsFunc function for the given Go type and
+// parquet schema. The column path indicates the column that the function is
+// being generated for in the parquet schema.
+func writeRowsFuncOf(t reflect.Type, schema *Schema, path columnPath) writeRowsFunc {
+	switch t {
+	case reflect.TypeOf(deprecated.Int96{}):
+		return (*columnBufferWriter).writeRowsInt96
+	}
+
+	switch t.Kind() {
+	case reflect.Bool:
+		return (*columnBufferWriter).writeRowsBool
+
+	case reflect.Int, reflect.Uint:
+		return (*columnBufferWriter).writeRowsInt
+
+	case reflect.Int8, reflect.Uint8:
+		return (*columnBufferWriter).writeRowsInt8
+
+	case reflect.Int16, reflect.Uint16:
+		return (*columnBufferWriter).writeRowsInt16
+
+	case reflect.Int32, reflect.Uint32:
+		return (*columnBufferWriter).writeRowsInt32
+
+	case reflect.Int64, reflect.Uint64:
+		return (*columnBufferWriter).writeRowsInt64
+
+	case reflect.Float32:
+		return (*columnBufferWriter).writeRowsFloat32
+
+	case reflect.Float64:
+		return (*columnBufferWriter).writeRowsFloat64
+
+	case reflect.String:
+		return (*columnBufferWriter).writeRowsString
+
+	case reflect.Slice:
+		if t.Elem().Kind() == reflect.Uint8 {
+			return (*columnBufferWriter).writeRowsString
+		} else {
+			return writeRowsFuncOfSlice(t, schema, path)
+		}
+
+	case reflect.Array:
+		if t.Elem().Kind() == reflect.Uint8 {
+			return writeRowsFuncOfArray(t, schema, path)
+		}
+
+	case reflect.Pointer:
+		return writeRowsFuncOfPointer(t, schema, path)
+
+	case reflect.Struct:
+		return writeRowsFuncOfStruct(t, schema, path)
+
+	case reflect.Map:
+		return writeRowsFuncOfMap(t, schema, path)
+	}
+
+	panic("cannot convert Go values of type " + t.String() + " to parquet value")
+}
+
+func writeRowsFuncOfArray(t reflect.Type, schema *Schema, path columnPath) writeRowsFunc {
+	len := t.Len()
+	if len == 16 {
+		return (*columnBufferWriter).writeRowsUUID
+	}
+	return func(w *columnBufferWriter, rows array, size, offset uintptr, levels columnLevels) error {
+		return w.writeRowsArray(rows, size, offset, levels, len)
+	}
+}
+
+func writeRowsFuncOfOptional(t reflect.Type, schema *Schema, path columnPath, writeRows writeRowsFunc) writeRowsFunc {
+	nullIndex, nonNullIndex := nullIndexFuncOf(t), nonNullIndexFuncOf(t)
+	return func(w *columnBufferWriter, rows array, size, offset uintptr, levels columnLevels) error {
+		if rows.len == 0 {
+			return writeRows(w, rows, size, 0, levels)
+		}
+
+		nonNullLevels := levels
+		nonNullLevels.definitionLevel++
+		// In this function, we are dealing with optional values which are
+		// neither pointers nor slices; for example, a []int32 field marked
+		// "optional" in its parent struct.
+		//
+		// We need to find zero values, which should be represented as nulls
+		// in the parquet column. In order to minimize the calls to writeRows
+		// and maximize throughput, we use the nullIndex and nonNullIndex
+		// functions, which are type-specific implementations of the algorithm.
+		//
+		// Sections of the input that are contiguous nulls or non-nulls can be
+		// sent to a single call to writeRows to be written to the underlying
+		// buffer since they share the same definition level.
+		//
+		// This optimization is defeated by inputs alternating null and non-null
+		// sequences of single values, we do not expect this condition to be a
+		// common case.
+		for i := 0; i < rows.len; {
+			a := array{}
+			p := rows.index(i, size, 0)
+			j := i + nonNullIndex(array{ptr: p, len: rows.len - i})
+
+			if i < j {
+				a.ptr = p
+				a.len = j - i
+				if err := writeRows(w, a, size, 0, levels); err != nil {
+					return err
+				}
+			}
+
+			if j < rows.len {
+				p = rows.index(j, size, 0)
+				i = j
+				j = j + nullIndex(array{ptr: p, len: rows.len - j})
+				a.ptr = p
+				a.len = j - i
+				if err := writeRows(w, a, size, 0, nonNullLevels); err != nil {
+					return err
+				}
+			}
+
+			i = j
+		}
+
+		return nil
+	}
+}
+
+func writeRowsFuncOfPointer(t reflect.Type, schema *Schema, path columnPath) writeRowsFunc {
+	elemType := t.Elem()
+	elemSize := elemType.Size()
+	writeRows := writeRowsFuncOf(elemType, schema, path)
+
+	if len(path) == 0 {
+		// This code path is taken when generating a writeRowsFunc for a pointer
+		// type. In this case, we do not need to increase the definition level
+		// since we are not deailng with an optional field but a pointer to the
+		// row type.
+		return func(w *columnBufferWriter, rows array, size, _ uintptr, levels columnLevels) error {
+			if rows.len == 0 {
+				return writeRows(w, rows, size, 0, levels)
+			}
+
+			for i := 0; i < rows.len; i++ {
+				p := *(*unsafe.Pointer)(rows.index(i, size, 0))
+				a := array{}
+				if p != nil {
+					a.ptr = p
+					a.len = 1
+				}
+				if err := writeRows(w, a, elemSize, 0, levels); err != nil {
+					return err
+				}
+			}
+
+			return nil
+		}
+	}
+
+	return func(w *columnBufferWriter, rows array, size, offset uintptr, levels columnLevels) error {
+		if rows.len == 0 {
+			return writeRows(w, rows, size, 0, levels)
+		}
+
+		for i := 0; i < rows.len; i++ {
+			p := *(*unsafe.Pointer)(rows.index(i, size, offset))
+			a := array{}
+			elemLevels := levels
+			if p != nil {
+				a.ptr = p
+				a.len = 1
+				elemLevels.definitionLevel++
+			}
+			if err := writeRows(w, a, elemSize, 0, elemLevels); err != nil {
+				return err
+			}
+		}
+
+		return nil
+	}
+}
+
+func writeRowsFuncOfSlice(t reflect.Type, schema *Schema, path columnPath) writeRowsFunc {
+	elemType := t.Elem()
+	elemSize := elemType.Size()
+	writeRows := writeRowsFuncOf(elemType, schema, path)
+	return func(w *columnBufferWriter, rows array, size, offset uintptr, levels columnLevels) error {
+		if rows.len == 0 {
+			return writeRows(w, rows, size, 0, levels)
+		}
+
+		levels.repetitionDepth++
+
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			a := *(*array)(p)
+			n := a.len
+
+			elemLevels := levels
+			if n > 0 {
+				a.len = 1
+				elemLevels.definitionLevel++
+			}
+
+			if err := writeRows(w, a, elemSize, 0, elemLevels); err != nil {
+				return err
+			}
+
+			if n > 1 {
+				elemLevels.repetitionLevel = elemLevels.repetitionDepth
+				a.ptr = a.index(1, elemSize, 0)
+				a.len = n - 1
+
+				if err := writeRows(w, a, elemSize, 0, elemLevels); err != nil {
+					return err
+				}
+			}
+		}
+
+		return nil
+	}
+}
+
+func writeRowsFuncOfStruct(t reflect.Type, schema *Schema, path columnPath) writeRowsFunc {
+	type column struct {
+		columnIndex int16
+		optional    bool
+		offset      uintptr
+		writeRows   writeRowsFunc
+	}
+
+	fields := structFieldsOf(t)
+	columns := make([]column, len(fields))
+
+	for i, f := range fields {
+		optional := false
+		columnPath := path.append(f.Name)
+		forEachStructTagOption(f.Tag, func(option, _ string) {
+			switch option {
+			case "list":
+				columnPath = columnPath.append("list", "element")
+			case "optional":
+				optional = true
+			}
+		})
+
+		writeRows := writeRowsFuncOf(f.Type, schema, columnPath)
+		if optional {
+			switch f.Type.Kind() {
+			case reflect.Pointer, reflect.Slice:
+			default:
+				writeRows = writeRowsFuncOfOptional(f.Type, schema, columnPath, writeRows)
+			}
+		}
+
+		columnInfo := schema.mapping.lookup(columnPath)
+		columns[i] = column{
+			columnIndex: columnInfo.columnIndex,
+			offset:      f.Offset,
+			writeRows:   writeRows,
+		}
+	}
+
+	return func(w *columnBufferWriter, rows array, size, offset uintptr, levels columnLevels) error {
+		for _, column := range columns {
+			levels.columnIndex = column.columnIndex
+			if err := column.writeRows(w, rows, size, offset+column.offset, levels); err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+}
+
+func writeRowsFuncOfMap(t reflect.Type, schema *Schema, path columnPath) writeRowsFunc {
+	keyPath := path.append("key_value", "key")
+	keyType := t.Key()
+	keySize := keyType.Size()
+	writeKeys := writeRowsFuncOf(keyType, schema, keyPath)
+	keyColumnIndex := schema.mapping.lookup(keyPath).columnIndex
+
+	valuePath := path.append("key_value", "value")
+	valueType := t.Elem()
+	valueSize := valueType.Size()
+	writeValues := writeRowsFuncOf(valueType, schema, valuePath)
+	valueColumnIndex := schema.mapping.lookup(valuePath).columnIndex
+
+	writeKeyValues := func(w *columnBufferWriter, keys, values array, levels columnLevels) error {
+		levels.columnIndex = keyColumnIndex
+		if err := writeKeys(w, keys, keySize, 0, levels); err != nil {
+			return err
+		}
+		levels.columnIndex = valueColumnIndex
+		if err := writeValues(w, values, valueSize, 0, levels); err != nil {
+			return err
+		}
+		return nil
+	}
+
+	return func(w *columnBufferWriter, rows array, size, offset uintptr, levels columnLevels) error {
+		if rows.len == 0 {
+			return writeKeyValues(w, rows, rows, levels)
+		}
+
+		levels.repetitionDepth++
+		mapKey := reflect.New(keyType).Elem()
+		mapValue := reflect.New(valueType).Elem()
+
+		for i := 0; i < rows.len; i++ {
+			m := reflect.NewAt(t, rows.index(i, size, offset)).Elem()
+
+			if m.Len() == 0 {
+				if err := writeKeyValues(w, array{}, array{}, levels); err != nil {
+					return err
+				}
+			} else {
+				elemLevels := levels
+				elemLevels.definitionLevel++
+
+				for it := m.MapRange(); it.Next(); {
+					mapKey.SetIterKey(it)
+					mapValue.SetIterValue(it)
+
+					k := array{ptr: addressOf(mapKey), len: 1}
+					v := array{ptr: addressOf(mapValue), len: 1}
+
+					if err := writeKeyValues(w, k, v, elemLevels); err != nil {
+						return err
+					}
+
+					elemLevels.repetitionLevel = elemLevels.repetitionDepth
+				}
+			}
+		}
+
+		return nil
+	}
+}
+
+func addressOf(v reflect.Value) unsafe.Pointer {
+	return (*[2]unsafe.Pointer)(unsafe.Pointer(&v))[1]
+}
+
+func (w *columnBufferWriter) writeRowsBool(rows array, size, offset uintptr, levels columnLevels) (err error) {
+	if rows.len == 0 {
+		return w.writeRowsNull(levels)
+	}
+
+	switch c := w.columns[levels.columnIndex].(type) {
+	case *booleanColumnBuffer:
+		c.writeValues(rows, size, offset)
+	default:
+		w.reset()
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			v := makeValueBoolean(*(*bool)(p))
+			v.repetitionLevel = levels.repetitionLevel
+			v.definitionLevel = levels.definitionLevel
+			w.values = append(w.values, v)
+		}
+		_, err = w.columns[levels.columnIndex].WriteValues(w.values)
+	}
+
+	return err
+}
+
+func (w *columnBufferWriter) writeRowsInt(rows array, size, offset uintptr, levels columnLevels) (err error) {
+	if rows.len == 0 {
+		return w.writeRowsNull(levels)
+	}
+
+	switch c := w.columns[levels.columnIndex].(type) {
+	case *int64ColumnBuffer:
+		c.writeValues(rows, size, offset)
+	case *uint64ColumnBuffer:
+		c.writeValues(rows, size, offset)
+	default:
+		w.reset()
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			v := makeValueInt64(int64(*(*int)(p)))
+			v.repetitionLevel = levels.repetitionLevel
+			v.definitionLevel = levels.definitionLevel
+			w.values = append(w.values, v)
+		}
+		_, err = w.columns[levels.columnIndex].WriteValues(w.values)
+	}
+
+	return err
+}
+
+func (w *columnBufferWriter) writeRowsInt8(rows array, size, offset uintptr, levels columnLevels) (err error) {
+	if rows.len == 0 {
+		return w.writeRowsNull(levels)
+	}
+
+	switch c := w.columns[levels.columnIndex].(type) {
+	case *int32ColumnBuffer:
+		c.writeValues(rows, size, offset)
+	case *uint32ColumnBuffer:
+		c.writeValues(rows, size, offset)
+	default:
+		w.reset()
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			v := makeValueInt32(int32(*(*int8)(p)))
+			v.repetitionLevel = levels.repetitionLevel
+			v.definitionLevel = levels.definitionLevel
+			w.values = append(w.values, v)
+		}
+		_, err = w.columns[levels.columnIndex].WriteValues(w.values)
+	}
+
+	return err
+}
+
+func (w *columnBufferWriter) writeRowsInt16(rows array, size, offset uintptr, levels columnLevels) (err error) {
+	if rows.len == 0 {
+		return w.writeRowsNull(levels)
+	}
+
+	switch c := w.columns[levels.columnIndex].(type) {
+	case *int32ColumnBuffer:
+		c.writeValues(rows, size, offset)
+	case *uint32ColumnBuffer:
+		c.writeValues(rows, size, offset)
+	default:
+		w.reset()
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			v := makeValueInt32(int32(*(*int16)(p)))
+			v.repetitionLevel = levels.repetitionLevel
+			v.definitionLevel = levels.definitionLevel
+			w.values = append(w.values, v)
+		}
+		_, err = w.columns[levels.columnIndex].WriteValues(w.values)
+	}
+
+	return err
+}
+
+func (w *columnBufferWriter) writeRowsInt32(rows array, size, offset uintptr, levels columnLevels) (err error) {
+	if rows.len == 0 {
+		return w.writeRowsNull(levels)
+	}
+
+	switch c := w.columns[levels.columnIndex].(type) {
+	case *int32ColumnBuffer:
+		c.writeValues(rows, size, offset)
+	case *uint32ColumnBuffer:
+		c.writeValues(rows, size, offset)
+	default:
+		w.reset()
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			v := makeValueInt32(*(*int32)(p))
+			v.repetitionLevel = levels.repetitionLevel
+			v.definitionLevel = levels.definitionLevel
+			w.values = append(w.values, v)
+		}
+		_, err = w.columns[levels.columnIndex].WriteValues(w.values)
+	}
+
+	return err
+}
+
+func (w *columnBufferWriter) writeRowsInt64(rows array, size, offset uintptr, levels columnLevels) (err error) {
+	if rows.len == 0 {
+		return w.writeRowsNull(levels)
+	}
+
+	switch c := w.columns[levels.columnIndex].(type) {
+	case *int64ColumnBuffer:
+		c.writeValues(rows, size, offset)
+	case *uint64ColumnBuffer:
+		c.writeValues(rows, size, offset)
+	default:
+		w.reset()
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			v := makeValueInt64(*(*int64)(p))
+			v.repetitionLevel = levels.repetitionLevel
+			v.definitionLevel = levels.definitionLevel
+			w.values = append(w.values, v)
+		}
+		_, err = w.columns[levels.columnIndex].WriteValues(w.values)
+	}
+
+	return err
+}
+
+func (w *columnBufferWriter) writeRowsInt96(rows array, size, offset uintptr, levels columnLevels) (err error) {
+	if rows.len == 0 {
+		return w.writeRowsNull(levels)
+	}
+
+	switch c := w.columns[levels.columnIndex].(type) {
+	case *int96ColumnBuffer:
+		c.writeValues(rows, size, offset)
+	default:
+		w.reset()
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			v := makeValueInt96(*(*deprecated.Int96)(p))
+			v.repetitionLevel = levels.repetitionLevel
+			v.definitionLevel = levels.definitionLevel
+			w.values = append(w.values, v)
+		}
+		_, err = w.columns[levels.columnIndex].WriteValues(w.values)
+	}
+
+	return err
+}
+
+func (w *columnBufferWriter) writeRowsFloat32(rows array, size, offset uintptr, levels columnLevels) (err error) {
+	if rows.len == 0 {
+		return w.writeRowsNull(levels)
+	}
+
+	switch c := w.columns[levels.columnIndex].(type) {
+	case *floatColumnBuffer:
+		c.writeValues(rows, size, offset)
+	default:
+		w.reset()
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			v := makeValueFloat(*(*float32)(p))
+			v.repetitionLevel = levels.repetitionLevel
+			v.definitionLevel = levels.definitionLevel
+			w.values = append(w.values, v)
+		}
+		_, err = w.columns[levels.columnIndex].WriteValues(w.values)
+	}
+
+	return err
+}
+
+func (w *columnBufferWriter) writeRowsFloat64(rows array, size, offset uintptr, levels columnLevels) (err error) {
+	if rows.len == 0 {
+		return w.writeRowsNull(levels)
+	}
+
+	switch c := w.columns[levels.columnIndex].(type) {
+	case *doubleColumnBuffer:
+		c.writeValues(rows, size, offset)
+	default:
+		w.reset()
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			v := makeValueDouble(*(*float64)(p))
+			v.repetitionLevel = levels.repetitionLevel
+			v.definitionLevel = levels.definitionLevel
+			w.values = append(w.values, v)
+		}
+		_, err = w.columns[levels.columnIndex].WriteValues(w.values)
+	}
+
+	return err
+}
+
+func (w *columnBufferWriter) writeRowsString(rows array, size, offset uintptr, levels columnLevels) (err error) {
+	if rows.len == 0 {
+		return w.writeRowsNull(levels)
+	}
+
+	switch c := w.columns[levels.columnIndex].(type) {
+	case *byteArrayColumnBuffer:
+		c.writeValues(rows, size, offset)
+	default:
+		w.reset()
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			v := makeValueString(ByteArray, *(*string)(p))
+			v.repetitionLevel = levels.repetitionLevel
+			v.definitionLevel = levels.definitionLevel
+			w.values = append(w.values, v)
+		}
+		_, err = w.columns[levels.columnIndex].WriteValues(w.values)
+	}
+
+	return err
+}
+
+func (w *columnBufferWriter) writeRowsUUID(rows array, size, offset uintptr, levels columnLevels) (err error) {
+	if rows.len == 0 {
+		return w.writeRowsNull(levels)
+	}
+
+	switch c := w.columns[levels.columnIndex].(type) {
+	case *fixedLenByteArrayColumnBuffer:
+		c.writeValues128(rows, size, offset)
+	default:
+		w.reset()
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			v := makeValueByteArray(FixedLenByteArray, (*byte)(p), 16)
+			v.repetitionLevel = levels.repetitionLevel
+			v.definitionLevel = levels.definitionLevel
+			w.values = append(w.values, v)
+		}
+		_, err = w.columns[levels.columnIndex].WriteValues(w.values)
+	}
+
+	return err
+}
+
+func (w *columnBufferWriter) writeRowsArray(rows array, size, offset uintptr, levels columnLevels, len int) (err error) {
+	if rows.len == 0 {
+		return w.writeRowsNull(levels)
+	}
+
+	switch c := w.columns[levels.columnIndex].(type) {
+	case *fixedLenByteArrayColumnBuffer:
+		c.writeValues(rows, size, offset)
+	default:
+		w.reset()
+		for i := 0; i < rows.len; i++ {
+			p := rows.index(i, size, offset)
+			v := makeValueByteArray(FixedLenByteArray, (*byte)(p), len)
+			v.repetitionLevel = levels.repetitionLevel
+			v.definitionLevel = levels.definitionLevel
+			w.values = append(w.values, v)
+		}
+		_, err = w.columns[levels.columnIndex].WriteValues(w.values)
+	}
+
+	return err
+}
+
+func (w *columnBufferWriter) writeRowsNull(levels columnLevels) error {
+	w.reset()
+	w.values = append(w.values[:0], Value{
+		repetitionLevel: levels.repetitionLevel,
+		definitionLevel: levels.definitionLevel,
+	})
+	_, err := w.columns[levels.columnIndex].WriteValues(w.values)
+	return err
+}
+
+func (w *columnBufferWriter) reset() {
+	if len(w.values) > w.maxLen {
+		w.maxLen = len(w.values)
+	}
+	w.values = w.values[:0]
+}
+
+func (w *columnBufferWriter) clear() {
+	clearValues(w.values[:w.maxLen])
+	w.maxLen = 0
+}
diff --git a/column_path.go b/column_path.go
index 72d37e7..06f154a 100644
--- a/column_path.go
+++ b/column_path.go
@@ -4,8 +4,8 @@ import "strings"
 
 type columnPath []string
 
-func (path columnPath) append(name string) columnPath {
-	return append(path[:len(path):len(path)], name)
+func (path columnPath) append(names ...string) columnPath {
+	return append(path[:len(path):len(path)], names...)
 }
 
 func (path columnPath) equal(other columnPath) bool {
diff --git a/encoding/delta/byte_array.go b/encoding/delta/byte_array.go
index 2d35562..83ec707 100644
--- a/encoding/delta/byte_array.go
+++ b/encoding/delta/byte_array.go
@@ -101,9 +101,15 @@ func (e *ByteArrayEncoding) encode(dst []byte, numValues int, valueAt func(int)
 
 func (e *ByteArrayEncoding) DecodeByteArray(dst, src []byte) ([]byte, error) {
 	dst = dst[:0]
-	err := e.decode(src, func(value []byte) error {
-		dst = plain.AppendByteArray(dst, value)
-		return nil
+	err := e.decode(src, func(prefix, suffix []byte) ([]byte, error) {
+		n := len(prefix) + len(suffix)
+		b := [4]byte{}
+		plain.PutByteArrayLength(b[:], n)
+		dst = append(dst, b[:]...)
+		i := len(dst)
+		dst = append(dst, prefix...)
+		dst = append(dst, suffix...)
+		return dst[i:len(dst):len(dst)], nil
 	})
 	if err != nil {
 		err = encoding.Error(e, err)
@@ -116,12 +122,15 @@ func (e *ByteArrayEncoding) DecodeFixedLenByteArray(dst, src []byte, size int) (
 		return dst[:0], encoding.Error(e, encoding.ErrInvalidArgument)
 	}
 	dst = dst[:0]
-	err := e.decode(src, func(value []byte) error {
-		if len(value) != size {
-			return fmt.Errorf("cannot decode value of size %d into fixed-length byte array of size %d", len(value), size)
+	err := e.decode(src, func(prefix, suffix []byte) ([]byte, error) {
+		n := len(prefix) + len(suffix)
+		if n != size {
+			return nil, fmt.Errorf("cannot decode value of size %d into fixed-length byte array of size %d", n, size)
 		}
-		dst = append(dst, value...)
-		return nil
+		i := len(dst)
+		dst = append(dst, prefix...)
+		dst = append(dst, suffix...)
+		return dst[i:len(dst):len(dst)], nil
 	})
 	if err != nil {
 		err = encoding.Error(e, err)
@@ -129,7 +138,7 @@ func (e *ByteArrayEncoding) DecodeFixedLenByteArray(dst, src []byte, size int) (
 	return dst, err
 }
 
-func (e *ByteArrayEncoding) decode(src []byte, observe func([]byte) error) error {
+func (e *ByteArrayEncoding) decode(src []byte, observe func(prefix, suffix []byte) ([]byte, error)) error {
 	prefix := getInt32Buffer()
 	defer putInt32Buffer(prefix)
 
@@ -149,9 +158,7 @@ func (e *ByteArrayEncoding) decode(src []byte, observe func([]byte) error) error
 		return fmt.Errorf("number of prefix and lengths mismatch: %d != %d", len(prefix.values), len(length.values))
 	}
 
-	value := getBytesBuffer()
-	defer putBytesBuffer(value)
-
+	var lastValue []byte
 	for i, n := range length.values {
 		if int(n) < 0 {
 			return fmt.Errorf("invalid negative value length: %d", n)
@@ -164,15 +171,15 @@ func (e *ByteArrayEncoding) decode(src []byte, observe func([]byte) error) error
 		if int(p) < 0 {
 			return fmt.Errorf("invalid negative prefix length: %d", p)
 		}
-		if int(p) > value.Len() {
-			return fmt.Errorf("prefix length %d is larger than the last value of size %d", p, value.Len())
+		if int(p) > len(lastValue) {
+			return fmt.Errorf("prefix length %d is larger than the last value of size %d", p, len(lastValue))
 		}
 
-		value.Truncate(int(p))
-		value.Write(src[:n])
-		src = src[n:]
+		prefix := lastValue[:p:p]
+		suffix := src[:n:n]
+		src = src[n:len(src):len(src)]
 
-		if err := observe(value.Bytes()); err != nil {
+		if lastValue, err = observe(prefix, suffix); err != nil {
 			return err
 		}
 	}
diff --git a/encoding/delta/delta.go b/encoding/delta/delta.go
index 9191411..e2168a1 100644
--- a/encoding/delta/delta.go
+++ b/encoding/delta/delta.go
@@ -1,7 +1,6 @@
 package delta
 
 import (
-	"bytes"
 	"sync"
 )
 
@@ -20,7 +19,6 @@ func (buf *int32Buffer) decode(src []byte) ([]byte, error) {
 
 var (
 	int32BufferPool sync.Pool // *int32Buffer
-	bytesBufferPool sync.Pool // *bytes.Buffer
 )
 
 func getInt32Buffer() *int32Buffer {
@@ -38,17 +36,3 @@ func getInt32Buffer() *int32Buffer {
 func putInt32Buffer(b *int32Buffer) {
 	int32BufferPool.Put(b)
 }
-
-func getBytesBuffer() *bytes.Buffer {
-	b, _ := bytesBufferPool.Get().(*bytes.Buffer)
-	if b != nil {
-		b.Reset()
-	} else {
-		b = new(bytes.Buffer)
-	}
-	return b
-}
-
-func putBytesBuffer(b *bytes.Buffer) {
-	bytesBufferPool.Put(b)
-}
diff --git a/encoding/plain/plain.go b/encoding/plain/plain.go
index fbc09aa..5a29fc6 100644
--- a/encoding/plain/plain.go
+++ b/encoding/plain/plain.go
@@ -208,11 +208,18 @@ func AppendDouble(b []byte, v float64) []byte {
 }
 
 func AppendByteArray(b, v []byte) []byte {
-	i := len(b)
-	j := i + 4
-	b = append(b, 0, 0, 0, 0)
+	length := [ByteArrayLengthSize]byte{}
+	PutByteArrayLength(length[:], len(v))
+	b = append(b, length[:]...)
+	b = append(b, v...)
+	return b
+}
+
+func AppendByteArrayString(b []byte, v string) []byte {
+	length := [ByteArrayLengthSize]byte{}
+	PutByteArrayLength(length[:], len(v))
+	b = append(b, length[:]...)
 	b = append(b, v...)
-	PutByteArrayLength(b[i:j:j], len(v))
 	return b
 }
 
diff --git a/internal/unsafecast/unsafecast.go b/internal/unsafecast/unsafecast.go
index dc30b0c..1efeeab 100644
--- a/internal/unsafecast/unsafecast.go
+++ b/internal/unsafecast/unsafecast.go
@@ -65,3 +65,12 @@ func BytesToSlice[T any](data []byte) []T {
 func BytesToString(data []byte) string {
 	return *(*string)(unsafe.Pointer(&data))
 }
+
+// StringToBytes applies the inverse conversion of BytesToString.
+func StringToBytes(data string) []byte {
+	return *(*[]byte)(unsafe.Pointer(&slice{
+		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&data)),
+		len: len(data),
+		cap: len(data),
+	}))
+}
diff --git a/reader.go b/reader.go
index cbdfdd9..f87efaa 100644
--- a/reader.go
+++ b/reader.go
@@ -34,7 +34,7 @@ type Reader struct {
 	file     reader
 	read     reader
 	rowIndex int64
-	values   [1]Row
+	rowbuf   []Row
 }
 
 // NewReader constructs a parquet reader reading rows from the given
@@ -61,38 +61,21 @@ type Reader struct {
 //	}
 //
 func NewReader(input io.ReaderAt, options ...ReaderOption) *Reader {
-	f, _ := input.(*File)
-	if f == nil {
-		n, err := sizeOf(input)
-		if err != nil {
-			panic(err)
-		}
-		if f, err = OpenFile(input, n); err != nil {
-			panic(err)
-		}
-	}
-
 	c, err := NewReaderConfig(options...)
 	if err != nil {
 		panic(err)
 	}
 
-	column := f.Root()
-	schema := NewSchema(column.Name(), column)
-
-	r := &Reader{
-		file: reader{schema: schema},
+	f, err := openFile(input)
+	if err != nil {
+		panic(err)
 	}
 
-	switch rowGroups := f.RowGroups(); len(rowGroups) {
-	case 0:
-		r.file.rowGroup = newEmptyRowGroup(schema)
-	case 1:
-		r.file.rowGroup = rowGroups[0]
-	default:
-		// TODO: should we attempt to merge the row groups via MergeRowGroups
-		// to preserve the global order of sorting columns within the file?
-		r.file.rowGroup = MultiRowGroup(rowGroups...)
+	r := &Reader{
+		file: reader{
+			schema:   f.schema,
+			rowGroup: fileRowGroupOf(f),
+		},
 	}
 
 	if c.Schema != nil {
@@ -104,6 +87,31 @@ func NewReader(input io.ReaderAt, options ...ReaderOption) *Reader {
 	return r
 }
 
+func openFile(input io.ReaderAt) (*File, error) {
+	f, _ := input.(*File)
+	if f != nil {
+		return f, nil
+	}
+	n, err := sizeOf(input)
+	if err != nil {
+		return nil, err
+	}
+	return OpenFile(input, n)
+}
+
+func fileRowGroupOf(f *File) RowGroup {
+	switch rowGroups := f.RowGroups(); len(rowGroups) {
+	case 0:
+		return newEmptyRowGroup(f.Schema())
+	case 1:
+		return rowGroups[0]
+	default:
+		// TODO: should we attempt to merge the row groups via MergeRowGroups
+		// to preserve the global order of sorting columns within the file?
+		return MultiRowGroup(rowGroups...)
+	}
+}
+
 // NewRowGroupReader constructs a new Reader which reads rows from the RowGroup
 // passed as argument.
 func NewRowGroupReader(rowGroup RowGroup, options ...ReaderOption) *Reader {
@@ -166,7 +174,7 @@ func (r *Reader) Reset() {
 	r.file.Reset()
 	r.read.Reset()
 	r.rowIndex = 0
-	clearValues(r.values[0])
+	clearRows(r.rowbuf)
 }
 
 // Read reads the next row from r. The type of the row must match the schema
@@ -189,13 +197,19 @@ func (r *Reader) Read(row interface{}) error {
 		return fmt.Errorf("seeking reader to row %d: %w", r.rowIndex, err)
 	}
 
-	n, err := r.read.ReadRows(r.values[:])
+	if cap(r.rowbuf) == 0 {
+		r.rowbuf = make([]Row, 1)
+	} else {
+		r.rowbuf = r.rowbuf[:1]
+	}
+
+	n, err := r.read.ReadRows(r.rowbuf[:])
 	if n == 0 {
 		return err
 	}
 
 	r.rowIndex++
-	return r.read.schema.Reconstruct(row, r.values[0])
+	return r.read.schema.Reconstruct(row, r.rowbuf[0])
 }
 
 func (r *Reader) updateReadSchema(rowType reflect.Type) error {
@@ -338,7 +352,8 @@ func (r *reader) Close() (err error) {
 
 var (
 	_ Rows                = (*Reader)(nil)
-	_ RowReader           = (*reader)(nil)
-	_ RowSeeker           = (*reader)(nil)
 	_ RowReaderWithSchema = (*Reader)(nil)
+
+	_ RowReader = (*reader)(nil)
+	_ RowSeeker = (*reader)(nil)
 )
diff --git a/reader_go18.go b/reader_go18.go
new file mode 100644
index 0000000..80cd94a
--- /dev/null
+++ b/reader_go18.go
@@ -0,0 +1,167 @@
+//go:build go1.18
+
+package parquet
+
+import (
+	"io"
+	"reflect"
+)
+
+// GenericReader is similar to a Reader but uses a type parameter to define the
+// Go type representing the schema of rows being read.
+//
+// See GenericWriter for details about the benefits over the classic Reader API.
+type GenericReader[T any] struct {
+	base Reader
+	read readFunc[T]
+}
+
+// NewGenericReader is like NewReader but returns GenericReader[T] suited to write
+// rows of Go type T.
+//
+// The type parameter T should be a map, struct, or any. Any other types will
+// cause a panic at runtime. Type checking is a lot more effective when the
+// generic parameter is a struct type, using map and interface types is somewhat
+// similar to using a Writer.
+//
+// If the option list may explicitly declare a schema, it must be compatible
+// with the schema generated from T.
+func NewGenericReader[T any](input io.ReaderAt, options ...ReaderOption) *GenericReader[T] {
+	c, err := NewReaderConfig(options...)
+	if err != nil {
+		panic(err)
+	}
+
+	t := typeOf[T]()
+	if c.Schema == nil {
+		c.Schema = schemaOf(dereference(t))
+	}
+
+	f, err := openFile(input)
+	if err != nil {
+		panic(err)
+	}
+
+	r := &GenericReader[T]{
+		base: Reader{
+			file: reader{
+				schema:   c.Schema,
+				rowGroup: fileRowGroupOf(f),
+			},
+		},
+	}
+
+	if !nodesAreEqual(c.Schema, f.schema) {
+		r.base.file.rowGroup = convertRowGroupTo(r.base.file.rowGroup, c.Schema)
+	}
+
+	r.base.read.init(r.base.file.schema, r.base.file.rowGroup)
+	r.read = readFuncOf[T](t, r.base.file.schema)
+	return r
+}
+
+func NewGenericRowGroupReader[T any](rowGroup RowGroup, options ...ReaderOption) *GenericReader[T] {
+	c, err := NewReaderConfig(options...)
+	if err != nil {
+		panic(err)
+	}
+
+	t := typeOf[T]()
+	if c.Schema == nil {
+		c.Schema = schemaOf(dereference(t))
+	}
+
+	r := &GenericReader[T]{
+		base: Reader{
+			file: reader{
+				schema:   c.Schema,
+				rowGroup: rowGroup,
+			},
+		},
+	}
+
+	if !nodesAreEqual(c.Schema, rowGroup.Schema()) {
+		r.base.file.rowGroup = convertRowGroupTo(r.base.file.rowGroup, c.Schema)
+	}
+
+	r.base.read.init(r.base.file.schema, r.base.file.rowGroup)
+	r.read = readFuncOf[T](t, r.base.file.schema)
+	return r
+}
+
+func (r *GenericReader[T]) Reset() {
+	r.base.Reset()
+}
+
+func (r *GenericReader[T]) Read(rows []T) (int, error) {
+	return r.read(r, rows)
+}
+
+func (r *GenericReader[T]) ReadRows(rows []Row) (int, error) {
+	return r.base.ReadRows(rows)
+}
+
+func (r *GenericReader[T]) Schema() *Schema {
+	return r.base.Schema()
+}
+
+func (r *GenericReader[T]) NumRows() int64 {
+	return r.base.NumRows()
+}
+
+func (r *GenericReader[T]) SeekToRow(rowIndex int64) error {
+	return r.base.SeekToRow(rowIndex)
+}
+
+func (r *GenericReader[T]) Close() error {
+	return r.base.Close()
+}
+
+func (r *GenericReader[T]) readRows(rows []T) (int, error) {
+	if cap(r.base.rowbuf) < len(rows) {
+		r.base.rowbuf = make([]Row, len(rows))
+	} else {
+		r.base.rowbuf = r.base.rowbuf[:len(rows)]
+	}
+
+	n, err := r.base.ReadRows(r.base.rowbuf)
+	if n > 0 {
+		schema := r.base.Schema()
+
+		for i, row := range r.base.rowbuf[:n] {
+			if err := schema.Reconstruct(&rows[i], row); err != nil {
+				return i, err
+			}
+		}
+	}
+	return n, err
+}
+
+var (
+	_ Rows                = (*GenericReader[any])(nil)
+	_ RowReaderWithSchema = (*Reader)(nil)
+
+	_ Rows                = (*GenericReader[struct{}])(nil)
+	_ RowReaderWithSchema = (*GenericReader[struct{}])(nil)
+
+	_ Rows                = (*GenericReader[map[struct{}]struct{}])(nil)
+	_ RowReaderWithSchema = (*GenericReader[map[struct{}]struct{}])(nil)
+)
+
+type readFunc[T any] func(*GenericReader[T], []T) (int, error)
+
+func readFuncOf[T any](t reflect.Type, schema *Schema) readFunc[T] {
+	switch t.Kind() {
+	case reflect.Interface, reflect.Map:
+		return (*GenericReader[T]).readRows
+
+	case reflect.Struct:
+		return (*GenericReader[T]).readRows
+
+	case reflect.Pointer:
+		if e := t.Elem(); e.Kind() == reflect.Struct {
+			return (*GenericReader[T]).readRows
+		}
+	}
+	panic("cannot create reader for values of type " + t.String())
+}
diff --git a/row.go b/row.go
index adf2a90..b15ccb7 100644
--- a/row.go
+++ b/row.go
@@ -209,11 +209,7 @@ func copyRows(dst RowWriter, src RowReader, buf []Row) (written int64, err error
 		buf = make([]Row, defaultRowBufferSize)
 	}
 
-	defer func() {
-		for _, row := range buf {
-			clearValues(row)
-		}
-	}()
+	defer clearRows(buf)
 
 	for {
 		rn, err := src.ReadRows(buf)
@@ -240,6 +236,13 @@ func copyRows(dst RowWriter, src RowReader, buf []Row) (written int64, err error
 	}
 }
 
+func clearRows(rows []Row) {
+	for i, values := range rows {
+		clearValues(values)
+		rows[i] = values[:0]
+	}
+}
+
 func sourceSchemaOf(r RowReader) *Schema {
 	if rrs, ok := r.(RowReaderWithSchema); ok {
 		return rrs.Schema()
diff --git a/schema.go b/schema.go
index 93f5870..e985f7b 100644
--- a/schema.go
+++ b/schema.go
@@ -110,7 +110,7 @@ func NewSchema(name string, root Node) *Schema {
 }
 
 func dereference(t reflect.Type) reflect.Type {
-	if t.Kind() == reflect.Ptr {
+	for t.Kind() == reflect.Ptr {
 		t = t.Elem()
 	}
 	return t
@@ -196,12 +196,12 @@ func (s *Schema) GoType() reflect.Type { return s.root.GoType() }
 // parquet schema.
 func (s *Schema) Deconstruct(row Row, value interface{}) Row {
 	v := reflect.ValueOf(value)
-	if v.Kind() == reflect.Ptr {
+	for v.Kind() == reflect.Ptr {
 		if v.IsNil() {
 			v = reflect.Value{}
-		} else {
-			v = v.Elem()
+			break
 		}
+		v = v.Elem()
 	}
 	if s.deconstruct != nil {
 		row = s.deconstruct(row, levels{}, v)
@@ -227,9 +227,15 @@ func (s *Schema) Reconstruct(value interface{}, row Row) error {
 	if v.IsNil() {
 		panic("cannot reconstruct row into nil pointer of type " + v.Type().String())
 	}
+	for v.Kind() == reflect.Ptr {
+		if v.IsNil() {
+			v.Set(reflect.New(v.Type().Elem()))
+		}
+		v = v.Elem()
+	}
 	var err error
 	if s.reconstruct != nil {
-		row, err = s.reconstruct(v.Elem(), levels{}, row)
+		row, err = s.reconstruct(v, levels{}, row)
 		if len(row) > 0 && err == nil {
 			err = fmt.Errorf("%d values remain unused after reconstructing go value of type %s from parquet row", len(row), v.Type())
 		}
@@ -290,7 +296,7 @@ func structNodeOf(t reflect.Type) *structNode {
 }
 
 func structFieldsOf(t reflect.Type) []reflect.StructField {
-	fields := appendStructFields(t, nil, nil)
+	fields := appendStructFields(t, nil, nil, 0)
 
 	for i := range fields {
 		f := &fields[i]
@@ -306,13 +312,16 @@ func structFieldsOf(t reflect.Type) []reflect.StructField {
 	return fields
 }
 
-func appendStructFields(t reflect.Type, fields []reflect.StructField, index []int) []reflect.StructField {
+func appendStructFields(t reflect.Type, fields []reflect.StructField, index []int, offset uintptr) []reflect.StructField {
 	for i, n := 0, t.NumField(); i < n; i++ {
 		fieldIndex := index[:len(index):len(index)]
 		fieldIndex = append(fieldIndex, i)
 
-		if f := t.Field(i); f.Anonymous {
-			fields = appendStructFields(f.Type, fields, fieldIndex)
+		f := t.Field(i)
+		f.Offset += offset
+
+		if f.Anonymous {
+			fields = appendStructFields(f.Type, fields, fieldIndex, f.Offset)
 		} else if f.IsExported() {
 			f.Index = fieldIndex
 			fields = append(fields, f)
@@ -445,130 +454,121 @@ func makeStructField(f reflect.StructField) structField {
 		compressed = c
 	}
 
-	if tag := f.Tag.Get("parquet"); tag != "" {
-		var element Node
-		_, tag = split(tag) // skip the field name
-
-		for tag != "" {
-			option := ""
-			option, tag = split(tag)
-			option, args := splitOptionArgs(option)
-
-			switch option {
-			case "optional":
-				setOptional()
+	forEachStructTagOption(f.Tag, func(option, args string) {
+		switch option {
+		case "optional":
+			setOptional()
 
-			case "snappy":
-				setCompression(&Snappy)
+		case "snappy":
+			setCompression(&Snappy)
 
-			case "gzip":
-				setCompression(&Gzip)
+		case "gzip":
+			setCompression(&Gzip)
 
-			case "brotli":
-				setCompression(&Brotli)
+		case "brotli":
+			setCompression(&Brotli)
 
-			case "lz4":
-				setCompression(&Lz4Raw)
+		case "lz4":
+			setCompression(&Lz4Raw)
 
-			case "zstd":
-				setCompression(&Zstd)
+		case "zstd":
+			setCompression(&Zstd)
 
-			case "uncompressed":
-				setCompression(&Uncompressed)
+		case "uncompressed":
+			setCompression(&Uncompressed)
 
-			case "plain":
-				setEncoding(&Plain)
+		case "plain":
+			setEncoding(&Plain)
 
-			case "dict":
-				setEncoding(&RLEDictionary)
+		case "dict":
+			setEncoding(&RLEDictionary)
 
-			case "delta":
-				switch f.Type.Kind() {
-				case reflect.Int, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint32, reflect.Uint64:
-					setEncoding(&DeltaBinaryPacked)
-				case reflect.String:
+		case "delta":
+			switch f.Type.Kind() {
+			case reflect.Int, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint32, reflect.Uint64:
+				setEncoding(&DeltaBinaryPacked)
+			case reflect.String:
+				setEncoding(&DeltaByteArray)
+			case reflect.Slice:
+				if f.Type.Elem().Kind() == reflect.Uint8 { // []byte?
 					setEncoding(&DeltaByteArray)
-				case reflect.Slice:
-					if f.Type.Elem().Kind() == reflect.Uint8 { // []byte?
-						setEncoding(&DeltaByteArray)
-					} else {
-						throwInvalidFieldTag(f, option)
-					}
-				case reflect.Array:
-					if f.Type.Elem().Kind() == reflect.Uint8 { // [N]byte?
-						setEncoding(&DeltaByteArray)
-					} else {
-						throwInvalidFieldTag(f, option)
-					}
-				default:
+				} else {
 					throwInvalidFieldTag(f, option)
 				}
-
-			case "list":
-				switch f.Type.Kind() {
-				case reflect.Slice:
-					element = nodeOf(f.Type.Elem())
-					setNode(element)
-					setList()
-				default:
+			case reflect.Array:
+				if f.Type.Elem().Kind() == reflect.Uint8 { // [N]byte?
+					setEncoding(&DeltaByteArray)
+				} else {
 					throwInvalidFieldTag(f, option)
 				}
+			default:
+				throwInvalidFieldTag(f, option)
+			}
 
-			case "enum":
-				switch f.Type.Kind() {
-				case reflect.String:
-					setNode(Enum())
-				default:
-					throwInvalidFieldTag(f, option)
-				}
+		case "list":
+			switch f.Type.Kind() {
+			case reflect.Slice:
+				element := nodeOf(f.Type.Elem())
+				setNode(element)
+				setList()
+			default:
+				throwInvalidFieldTag(f, option)
+			}
 
-			case "uuid":
-				switch f.Type.Kind() {
-				case reflect.Array:
-					if f.Type.Elem().Kind() != reflect.Uint8 || f.Type.Len() != 16 {
-						throwInvalidFieldTag(f, option)
-					}
-				default:
-					throwInvalidFieldTag(f, option)
-				}
+		case "enum":
+			switch f.Type.Kind() {
+			case reflect.String:
+				setNode(Enum())
+			default:
+				throwInvalidFieldTag(f, option)
+			}
 
-			case "decimal":
-				scale, precision, err := parseDecimalArgs(args)
-				if err != nil {
-					throwInvalidFieldTag(f, option+args)
-				}
-				var baseType Type
-				switch f.Type.Kind() {
-				case reflect.Int32:
-					baseType = Int32Type
-				case reflect.Int64:
-					baseType = Int64Type
-				case reflect.Array:
-					baseType = FixedLenByteArrayType(calcDecimalFixedLenByteArraySize(precision))
-				default:
+		case "uuid":
+			switch f.Type.Kind() {
+			case reflect.Array:
+				if f.Type.Elem().Kind() != reflect.Uint8 || f.Type.Len() != 16 {
 					throwInvalidFieldTag(f, option)
 				}
+			default:
+				throwInvalidFieldTag(f, option)
+			}
 
-				setNode(Decimal(scale, precision, baseType))
-			case "date":
-				switch f.Type.Kind() {
-				case reflect.Int32:
-					setNode(Date())
-				default:
-					throwInvalidFieldTag(f, option)
-				}
-			case "timestamp":
-				switch f.Type.Kind() {
-				case reflect.Int64:
-					setNode(Timestamp(Millisecond))
-				default:
-					throwInvalidFieldTag(f, option)
-				}
+		case "decimal":
+			scale, precision, err := parseDecimalArgs(args)
+			if err != nil {
+				throwInvalidFieldTag(f, option+args)
+			}
+			var baseType Type
+			switch f.Type.Kind() {
+			case reflect.Int32:
+				baseType = Int32Type
+			case reflect.Int64:
+				baseType = Int64Type
+			case reflect.Array:
+				baseType = FixedLenByteArrayType(calcDecimalFixedLenByteArraySize(precision))
+			default:
+				throwInvalidFieldTag(f, option)
+			}
+
+			setNode(Decimal(scale, precision, baseType))
+		case "date":
+			switch f.Type.Kind() {
+			case reflect.Int32:
+				setNode(Date())
 			default:
-				throwUnknownFieldTag(f, option)
+				throwInvalidFieldTag(f, option)
 			}
+		case "timestamp":
+			switch f.Type.Kind() {
+			case reflect.Int64:
+				setNode(Timestamp(Millisecond))
+			default:
+				throwInvalidFieldTag(f, option)
+			}
+		default:
+			throwUnknownFieldTag(f, option)
 		}
-	}
+	})
 
 	if field.Node == nil {
 		field.Node = nodeOf(f.Type)
@@ -593,6 +593,18 @@ func makeStructField(f reflect.StructField) structField {
 	return field
 }
 
+func forEachStructTagOption(st reflect.StructTag, do func(option, args string)) {
+	if tag := st.Get("parquet"); tag != "" {
+		_, tag = split(tag) // skip the field name
+		for tag != "" {
+			option := ""
+			option, tag = split(tag)
+			option, args := splitOptionArgs(option)
+			do(option, args)
+		}
+	}
+}
+
 func nodeOf(t reflect.Type) Node {
 	switch t {
 	case reflect.TypeOf(deprecated.Int96{}):
diff --git a/value.go b/value.go
index f1053c6..6c0a339 100644
--- a/value.go
+++ b/value.go
@@ -367,6 +367,9 @@ func (v Value) Kind() Kind { return ^Kind(v.kind) }
 // IsNull returns true if v is the null value.
 func (v Value) IsNull() bool { return v.kind == 0 }
 
+// Byte returns v as a byte, which may truncate the underlying byte.
+func (v Value) Byte() byte { return byte(v.u64) }
+
 // Boolean returns v as a bool, assuming the underlying type is BOOLEAN.
 func (v Value) Boolean() bool { return v.u64 != 0 }
 
diff --git a/writer.go b/writer.go
index 4ddcb6a..e82dd6a 100644
--- a/writer.go
+++ b/writer.go
@@ -45,7 +45,7 @@ type Writer struct {
 	config *WriterConfig
 	schema *Schema
 	writer *writer
-	buffer [defaultRowBufferSize]Row
+	rowbuf []Row
 }
 
 // NewWriter constructs a parquet writer writing a file to the given io.Writer.
@@ -130,15 +130,18 @@ func (w *Writer) Write(row interface{}) error {
 	if w.schema == nil {
 		w.configure(SchemaOf(row))
 	}
-	defer func() {
-		clearValues(w.buffer[1])
-	}()
-	w.buffer[0] = w.schema.Deconstruct(w.buffer[0][:0], row)
-	_, err := w.WriteRows(w.buffer[:1])
+	if cap(w.rowbuf) == 0 {
+		w.rowbuf = make([]Row, 1)
+	} else {
+		w.rowbuf = w.rowbuf[:1]
+	}
+	defer clearRows(w.rowbuf)
+	w.rowbuf[0] = w.schema.Deconstruct(w.rowbuf[0][:0], row)
+	_, err := w.WriteRows(w.rowbuf)
 	return err
 }
 
-// WriteRow is called to write another row to the parquet file.
+// WriteRows is called to write rows to the parquet file.
 //
 // The Writer must have been given a schema when NewWriter was called, otherwise
 // the structure of the parquet file cannot be determined from the row only.
@@ -188,12 +191,12 @@ func (w *Writer) ReadRowsFrom(rows RowReader) (written int64, err error) {
 			w.configure(r.Schema())
 		}
 	}
-	defer func() {
-		for _, row := range w.buffer {
-			clearValues(row)
-		}
-	}()
-	return copyRows(w.writer, rows, w.buffer[:])
+	if cap(w.rowbuf) < defaultRowBufferSize {
+		w.rowbuf = make([]Row, defaultRowBufferSize)
+	} else {
+		w.rowbuf = w.rowbuf[:cap(w.rowbuf)]
+	}
+	return copyRows(w.writer, rows, w.rowbuf)
 }
 
 // Schema returns the schema of rows written by w.
diff --git a/writer_go18.go b/writer_go18.go
new file mode 100644
index 0000000..fe81a75
--- /dev/null
+++ b/writer_go18.go
@@ -0,0 +1,211 @@
+//go:build go1.18
+
+package parquet
+
+import (
+	"io"
+	"reflect"
+)
+
+// GenericWriter is similar to a Writer but uses a type parameter to define the
+// Go type representing the schema of rows being written.
+//
+// Using this type over Writer has multiple advantages:
+//
+// - By leveraging type information, the Go compiler can provide greater
+//   guarantees that the code is correct. For example, the parquet.Writer.Write
+//   method accepts an argument of type interface{}, which delays type checking
+//   until runtime. The parquet.GenericWriter[T].Write method ensures at
+//   compile time that the values it receives will be of type T, reducing the
+//   risk of introducing errors.
+//
+// - Since type information is known at compile time, the implementation of
+//   parquet.GenericWriter[T] can make safe assumptions, removing the need for
+//   runtime validation of how the parameters are passed to its methods.
+//   Optimizations relying on type information are more effective, some of the
+//   writer's state can be precomputed at initialization, which was not possible
+//   with parquet.Writer.
+//
+// - The parquet.GenericWriter[T].Write method uses a data-oriented design,
+//   accepting an slice of T instead of a single value, creating more
+//   opportunities to amortize the runtime cost of abstractions.
+//   This optimization is not available for parquet.Writer because its Write
+//   method's argument would be of type []interface{}, which would require
+//   conversions back and forth from concrete types to empty interfaces (since
+//   a []T cannot be interpreted as []interface{} in Go), would make the API
+//   more difficult to use and waste compute resources in the type conversions,
+//   defeating the purpose of the optimization in the first place.
+//
+// Note that this type is only available when compiling with Go 1.18 or later.
+type GenericWriter[T any] struct {
+	// At this time GenericWriter is expressed in terms of Writer to reuse the
+	// underlying logic. In the future, and if we accepted to break backward
+	// compatibility on the Write method, we could modify Writer to be an alias
+	// to GenericWriter with:
+	//
+	//	type Writer = GenericWriter[any]
+	//
+	base Writer
+	// This function writes rows of type T to the writer, it gets generated by
+	// the NewGenericWriter function based on the type T and the underlying
+	// schema of the parquet file.
+	write writeFunc[T]
+	// This field is used to leverage the optimized writeRowsFunc algorithms.
+	buffers columnBufferWriter
+}
+
+// NewGenericWriter is like NewWriter but returns a GenericWriter[T] suited to
+// write rows of Go type T.
+//
+// The type parameter T should be a map, struct, or any. Any other types will
+// cause a panic at runtime. Type checking is a lot more effective when the
+// generic parameter is a struct type, using map and interface types is somewhat
+// similar to using a Writer.
+//
+// If the option list may explicitly declare a schema, it must be compatible
+// with the schema generated from T.
+func NewGenericWriter[T any](output io.Writer, options ...WriterOption) *GenericWriter[T] {
+	config, err := NewWriterConfig(options...)
+	if err != nil {
+		panic(err)
+	}
+
+	t := typeOf[T]()
+	schema := schemaOf(dereference(t))
+	if config.Schema == nil {
+		config.Schema = schema
+	}
+
+	return &GenericWriter[T]{
+		base: Writer{
+			output: output,
+			config: config,
+			schema: schema,
+			writer: newWriter(output, config),
+		},
+		write: writeFuncOf[T](t, config.Schema),
+	}
+}
+
+type writeFunc[T any] func(*GenericWriter[T], []T) (int, error)
+
+func writeFuncOf[T any](t reflect.Type, schema *Schema) writeFunc[T] {
+	switch t.Kind() {
+	case reflect.Interface, reflect.Map:
+		return (*GenericWriter[T]).writeRows
+
+	case reflect.Struct:
+		return makeWriteFunc[T](t, schema)
+
+	case reflect.Pointer:
+		if e := t.Elem(); e.Kind() == reflect.Struct {
+			return makeWriteFunc[T](t, schema)
+		}
+	}
+	panic("cannot create writer for values of type " + t.String())
+}
+
+func makeWriteFunc[T any](t reflect.Type, schema *Schema) writeFunc[T] {
+	size := t.Size()
+	writeRows := writeRowsFuncOf(t, schema, nil)
+	return func(w *GenericWriter[T], rows []T) (n int, err error) {
+		defer w.buffers.clear()
+		err = writeRows(&w.buffers, makeArray(rows), size, 0, columnLevels{})
+		if err == nil {
+			n = len(rows)
+		}
+		return n, err
+	}
+}
+
+func (w *GenericWriter[T]) Close() error {
+	return w.base.Close()
+}
+
+func (w *GenericWriter[T]) Flush() error {
+	return w.base.Flush()
+}
+
+func (w *GenericWriter[T]) Reset(output io.Writer) {
+	w.base.Reset(output)
+}
+
+func (w *GenericWriter[T]) Write(rows []T) (int, error) {
+	if w.buffers.columns == nil {
+		w.buffers = columnBufferWriter{
+			columns: make([]ColumnBuffer, len(w.base.writer.columns)),
+			values:  make([]Value, 0, defaultValueBufferSize),
+		}
+
+		for i, c := range w.base.writer.columns {
+			// These fields are usually lazily initialized when writing rows,
+			// we need them to exist now tho.
+			c.columnBuffer = c.newColumnBuffer()
+			c.maxValues = int32(c.columnBuffer.Cap())
+			w.buffers.columns[i] = c.columnBuffer
+		}
+	}
+
+	n, err := w.write(w, rows)
+	if err != nil {
+		return n, err
+	}
+
+	for _, c := range w.base.writer.columns {
+		c.numValues = int32(c.columnBuffer.NumValues())
+
+		if c.numValues > 0 && c.numValues >= c.maxValues {
+			if err := c.flush(); err != nil {
+				return 0, err
+			}
+		}
+	}
+
+	return n, nil
+}
+
+func (w *GenericWriter[T]) WriteRows(rows []Row) (int, error) {
+	return w.base.WriteRows(rows)
+}
+
+func (w *GenericWriter[T]) WriteRowGroup(rowGroup RowGroup) (int64, error) {
+	return w.base.WriteRowGroup(rowGroup)
+}
+
+func (w *GenericWriter[T]) ReadRowsFrom(rows RowReader) (int64, error) {
+	return w.base.ReadRowsFrom(rows)
+}
+
+func (w *GenericWriter[T]) Schema() *Schema {
+	return w.base.Schema()
+}
+
+func (w *GenericWriter[T]) writeRows(rows []T) (int, error) {
+	if cap(w.base.rowbuf) < len(rows) {
+		w.base.rowbuf = make([]Row, len(rows))
+	} else {
+		w.base.rowbuf = w.base.rowbuf[:len(rows)]
+	}
+	defer clearRows(w.base.rowbuf)
+
+	schema := w.base.Schema()
+	for i := range rows {
+		w.base.rowbuf[i] = schema.Deconstruct(w.base.rowbuf[i], &rows[i])
+	}
+
+	return w.base.WriteRows(w.base.rowbuf)
+}
+
+var (
+	_ RowWriterWithSchema = (*GenericWriter[any])(nil)
+	_ RowReaderFrom       = (*GenericWriter[any])(nil)
+	_ RowGroupWriter      = (*GenericWriter[any])(nil)
+
+	_ RowWriterWithSchema = (*GenericWriter[struct{}])(nil)
+	_ RowReaderFrom       = (*GenericWriter[struct{}])(nil)
+	_ RowGroupWriter      = (*GenericWriter[struct{}])(nil)
+
+	_ RowWriterWithSchema = (*GenericWriter[map[struct{}]struct{}])(nil)
+	_ RowReaderFrom       = (*GenericWriter[map[struct{}]struct{}])(nil)
+	_ RowGroupWriter      = (*GenericWriter[map[struct{}]struct{}])(nil)
+)


Test patch:
diff --git a/buffer_go18_test.go b/buffer_go18_test.go
new file mode 100644
index 0000000..3d83e98
--- /dev/null
+++ b/buffer_go18_test.go
@@ -0,0 +1,163 @@
+//go:build go1.18
+
+package parquet_test
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"math/rand"
+	"reflect"
+	"testing"
+	"testing/quick"
+
+	"github.com/segmentio/parquet-go"
+)
+
+func TestGenericBuffer(t *testing.T) {
+	testGenericBuffer[booleanColumn](t)
+	testGenericBuffer[int32Column](t)
+	testGenericBuffer[int64Column](t)
+	testGenericBuffer[int96Column](t)
+	testGenericBuffer[floatColumn](t)
+	testGenericBuffer[doubleColumn](t)
+	testGenericBuffer[byteArrayColumn](t)
+	testGenericBuffer[fixedLenByteArrayColumn](t)
+	testGenericBuffer[stringColumn](t)
+	testGenericBuffer[indexedStringColumn](t)
+	testGenericBuffer[uuidColumn](t)
+	testGenericBuffer[mapColumn](t)
+	testGenericBuffer[decimalColumn](t)
+	testGenericBuffer[addressBook](t)
+	testGenericBuffer[contact](t)
+	testGenericBuffer[listColumn2](t)
+	testGenericBuffer[listColumn1](t)
+	testGenericBuffer[listColumn0](t)
+	testGenericBuffer[nestedListColumn1](t)
+	testGenericBuffer[nestedListColumn](t)
+	testGenericBuffer[*contact](t)
+}
+
+func testGenericBuffer[Row any](t *testing.T) {
+	var prng = rand.New(rand.NewSource(1))
+	var model Row
+	t.Run(reflect.TypeOf(model).Name(), func(t *testing.T) {
+		f := func(rows []Row) bool {
+			if len(rows) == 0 {
+				return true // TODO: fix support for parquet files with zero rows
+			}
+			if err := testGenericBufferRows(rows); err != nil {
+				t.Error(err)
+				return false
+			}
+			return true
+		}
+		if err := quick.Check(f, &quick.Config{Rand: prng}); err != nil {
+			t.Error(err)
+		}
+	})
+}
+
+func testGenericBufferRows[Row any](rows []Row) error {
+	setNullPointers(rows)
+	buffer := parquet.NewGenericBuffer[Row]()
+	_, err := buffer.Write(rows)
+	if err != nil {
+		return err
+	}
+	reader := parquet.NewGenericRowGroupReader[Row](buffer)
+	result := make([]Row, len(rows))
+	n, err := reader.Read(result)
+	if err != nil && !errors.Is(err, io.EOF) {
+		return err
+	}
+	if n < len(rows) {
+		return fmt.Errorf("not enough values were read: want=%d got=%d", len(rows), n)
+	}
+	if !reflect.DeepEqual(rows, result) {
+		return fmt.Errorf("rows mismatch:\nwant: %#v\ngot:  %#v", rows, result)
+	}
+	return nil
+}
+
+func setNullPointers[Row any](rows []Row) {
+	if len(rows) > 0 && reflect.TypeOf(rows[0]).Kind() == reflect.Pointer {
+		for i := range rows {
+			v := reflect.ValueOf(&rows[i]).Elem()
+			if v.IsNil() {
+				v.Set(reflect.New(v.Type().Elem()))
+			}
+		}
+	}
+}
+
+type generator[T any] interface {
+	generate(*rand.Rand) T
+}
+
+func BenchmarkGenericBuffer(b *testing.B) {
+	benchmarkGenericBuffer[benchmarkRowType](b)
+	benchmarkGenericBuffer[booleanColumn](b)
+	benchmarkGenericBuffer[int32Column](b)
+	benchmarkGenericBuffer[int64Column](b)
+	benchmarkGenericBuffer[floatColumn](b)
+	benchmarkGenericBuffer[doubleColumn](b)
+	benchmarkGenericBuffer[byteArrayColumn](b)
+	benchmarkGenericBuffer[fixedLenByteArrayColumn](b)
+	benchmarkGenericBuffer[stringColumn](b)
+	benchmarkGenericBuffer[indexedStringColumn](b)
+	benchmarkGenericBuffer[uuidColumn](b)
+	benchmarkGenericBuffer[mapColumn](b)
+	benchmarkGenericBuffer[decimalColumn](b)
+	benchmarkGenericBuffer[contact](b)
+}
+
+func benchmarkGenericBuffer[Row generator[Row]](b *testing.B) {
+	var model Row
+	b.Run(reflect.TypeOf(model).Name(), func(b *testing.B) {
+		prng := rand.New(rand.NewSource(0))
+		rows := make([]Row, benchmarkNumRows)
+		for i := range rows {
+			rows[i] = rows[i].generate(prng)
+		}
+
+		b.Run("go1.17", func(b *testing.B) {
+			buffer := parquet.NewBuffer(parquet.SchemaOf(rows[0]))
+			i := 0
+			benchmarkRowsPerSecond(b, func() int {
+				for j := 0; j < benchmarkRowsPerStep; j++ {
+					if err := buffer.Write(&rows[i]); err != nil {
+						b.Fatal(err)
+					}
+				}
+
+				i += benchmarkRowsPerStep
+				i %= benchmarkNumRows
+
+				if i == 0 {
+					buffer.Reset()
+				}
+				return benchmarkRowsPerStep
+			})
+		})
+
+		b.Run("go1.18", func(b *testing.B) {
+			buffer := parquet.NewGenericBuffer[Row]()
+			i := 0
+			benchmarkRowsPerSecond(b, func() int {
+				n, err := buffer.Write(rows[i : i+benchmarkRowsPerStep])
+				if err != nil {
+					b.Fatal(err)
+				}
+
+				i += benchmarkRowsPerStep
+				i %= benchmarkNumRows
+
+				if i == 0 {
+					buffer.Reset()
+				}
+				return n
+			})
+		})
+	})
+}
diff --git a/buffer_test.go b/buffer_test.go
index 9b5154e..77fa7e5 100644
--- a/buffer_test.go
+++ b/buffer_test.go
@@ -11,7 +11,6 @@ import (
 	"strconv"
 	"testing"
 	"testing/quick"
-	"time"
 
 	"github.com/segmentio/parquet-go"
 	"github.com/segmentio/parquet-go/encoding"
@@ -553,18 +552,8 @@ func TestBufferSeekToRow(t *testing.T) {
 	}
 }
 
-type benchmarkBufferRowType struct {
-	ID    [16]byte `parquet:"id,uuid"`
-	Value float64  `parquet:"value"`
-}
-
-const (
-	benchmarkBufferNumRows     = 10_000
-	benchmarkBufferRowsPerStep = 100
-)
-
 func generateBenchmarkBufferRows(n int) (*parquet.Schema, []parquet.Row) {
-	model := new(benchmarkBufferRowType)
+	model := new(benchmarkRowType)
 	schema := parquet.SchemaOf(model)
 	prng := rand.New(rand.NewSource(0))
 	rows := make([]parquet.Row, n)
@@ -580,24 +569,21 @@ func generateBenchmarkBufferRows(n int) (*parquet.Schema, []parquet.Row) {
 }
 
 func BenchmarkBufferReadRows100x(b *testing.B) {
-	schema, rows := generateBenchmarkBufferRows(benchmarkBufferNumRows)
+	schema, rows := generateBenchmarkBufferRows(benchmarkNumRows)
 	buffer := parquet.NewBuffer(schema)
 
-	for i := 0; i < len(rows); i += benchmarkBufferRowsPerStep {
-		j := i + benchmarkBufferRowsPerStep
+	for i := 0; i < len(rows); i += benchmarkRowsPerStep {
+		j := i + benchmarkRowsPerStep
 		if _, err := buffer.WriteRows(rows[i:j]); err != nil {
 			b.Fatal(err)
 		}
 	}
 
-	b.ResetTimer()
-	start := time.Now()
-
 	bufferRows := buffer.Rows()
 	defer bufferRows.Close()
 
-	for i := 0; i < b.N; i++ {
-		_, err := bufferRows.ReadRows(rows[:benchmarkBufferRowsPerStep])
+	benchmarkRowsPerSecond(b, func() int {
+		n, err := bufferRows.ReadRows(rows[:benchmarkRowsPerStep])
 		if err != nil {
 			if errors.Is(err, io.EOF) {
 				err = bufferRows.SeekToRow(0)
@@ -606,35 +592,27 @@ func BenchmarkBufferReadRows100x(b *testing.B) {
 				b.Fatal(err)
 			}
 		}
-	}
-
-	numRows := b.N * benchmarkBufferRowsPerStep
-	seconds := time.Since(start).Seconds()
-	b.ReportMetric(float64(numRows)/seconds, "row/s")
+		return n
+	})
 }
 
 func BenchmarkBufferWriteRows100x(b *testing.B) {
-	schema, rows := generateBenchmarkBufferRows(benchmarkBufferNumRows)
+	schema, rows := generateBenchmarkBufferRows(benchmarkNumRows)
 	buffer := parquet.NewBuffer(schema)
 
-	b.ResetTimer()
-	start := time.Now()
-
-	for i, j := 0, 0; i < b.N; i++ {
-		_, err := buffer.WriteRows(rows[j : j+benchmarkBufferRowsPerStep])
+	i := 0
+	benchmarkRowsPerSecond(b, func() int {
+		n, err := buffer.WriteRows(rows[i : i+benchmarkRowsPerStep])
 		if err != nil {
 			b.Fatal(err)
 		}
 
-		j += benchmarkBufferRowsPerStep
-		j %= benchmarkBufferNumRows
+		i += benchmarkRowsPerStep
+		i %= benchmarkNumRows
 
-		if j == 0 {
+		if i == 0 {
 			buffer.Reset()
 		}
-	}
-
-	numRows := b.N * benchmarkBufferRowsPerStep
-	seconds := time.Since(start).Seconds()
-	b.ReportMetric(float64(numRows)/seconds, "row/s")
+		return n
+	})
 }
diff --git a/merge_test.go b/merge_test.go
index dcb1a55..52db9d3 100644
--- a/merge_test.go
+++ b/merge_test.go
@@ -16,7 +16,7 @@ import (
 
 const (
 	numRowGroups = 3
-	rowsPerGroup = benchmarkReaderNumRows
+	rowsPerGroup = benchmarkNumRows
 )
 
 func BenchmarkMergeRowGroups(b *testing.B) {
@@ -46,11 +46,11 @@ func BenchmarkMergeRowGroups(b *testing.B) {
 					start := time.Now()
 
 					rows := mergedRowGroup.Rows()
-					rbuf := make([]parquet.Row, 20)
+					rbuf := make([]parquet.Row, benchmarkRowsPerStep)
 					defer func() { rows.Close() }()
 
-					for i := 0; i < b.N; i++ {
-						_, err := rows.ReadRows(rbuf)
+					benchmarkRowsPerSecond(b, func() int {
+						n, err := rows.ReadRows(rbuf)
 						if err != nil {
 							if !errors.Is(err, io.EOF) {
 								b.Fatal(err)
@@ -58,7 +58,8 @@ func BenchmarkMergeRowGroups(b *testing.B) {
 							rows.Close()
 							rows = mergedRowGroup.Rows()
 						}
-					}
+						return n
+					})
 
 					seconds := time.Since(start).Seconds()
 					b.ReportMetric(float64(b.N)/seconds, "row/s")
@@ -117,11 +118,11 @@ func BenchmarkMergeFiles(b *testing.B) {
 					start := time.Now()
 
 					rows := mergedRowGroup.Rows()
-					rbuf := make([]parquet.Row, 0, 20)
+					rbuf := make([]parquet.Row, benchmarkRowsPerStep)
 					defer func() { rows.Close() }()
 
-					for i := 0; i < b.N; i++ {
-						_, err := rows.ReadRows(rbuf)
+					benchmarkRowsPerSecond(b, func() int {
+						n, err := rows.ReadRows(rbuf)
 						if err != nil {
 							if !errors.Is(err, io.EOF) {
 								b.Fatal(err)
@@ -129,7 +130,8 @@ func BenchmarkMergeFiles(b *testing.B) {
 							rows.Close()
 							rows = mergedRowGroup.Rows()
 						}
-					}
+						return n
+					})
 
 					totalSize := int64(0)
 					for _, f := range files[:n] {
@@ -138,7 +140,7 @@ func BenchmarkMergeFiles(b *testing.B) {
 
 					seconds := time.Since(start).Seconds()
 					b.ReportMetric(float64(b.N)/seconds, "row/s")
-					b.SetBytes(int64(math.Ceil(float64(totalSize) / benchmarkReaderNumRows)))
+					b.SetBytes(int64(math.Ceil(float64(totalSize) / benchmarkNumRows)))
 				})
 			}
 		})
diff --git a/parquet_test.go b/parquet_test.go
index c2cf442..134af2e 100644
--- a/parquet_test.go
+++ b/parquet_test.go
@@ -6,11 +6,202 @@ import (
 	"io"
 	"math/rand"
 	"reflect"
+	"strings"
+	"testing"
+	"time"
 
+	"github.com/google/uuid"
 	"github.com/segmentio/parquet-go"
 	"github.com/segmentio/parquet-go/deprecated"
 )
 
+const (
+	benchmarkNumRows     = 10_000
+	benchmarkRowsPerStep = 100
+)
+
+type benchmarkRowType struct {
+	ID    [16]byte `parquet:"id,uuid"`
+	Value float64  `parquet:"value"`
+}
+
+func (row benchmarkRowType) generate(prng *rand.Rand) benchmarkRowType {
+	prng.Read(row.ID[:])
+	row.Value = prng.Float64()
+	return row
+}
+
+type booleanColumn struct {
+	Value bool
+}
+
+func (row booleanColumn) generate(prng *rand.Rand) booleanColumn {
+	return booleanColumn{Value: prng.Int()%2 == 0}
+}
+
+type int32Column struct {
+	Value int32
+}
+
+func (row int32Column) generate(prng *rand.Rand) int32Column {
+	return int32Column{Value: prng.Int31n(100)}
+}
+
+type int64Column struct {
+	Value int64
+}
+
+func (row int64Column) generate(prng *rand.Rand) int64Column {
+	return int64Column{Value: prng.Int63n(100)}
+}
+
+type int96Column struct {
+	Value deprecated.Int96
+}
+
+func (row int96Column) generate(prng *rand.Rand) int96Column {
+	row.Value[0] = prng.Uint32()
+	row.Value[1] = prng.Uint32()
+	row.Value[2] = prng.Uint32()
+	return row
+}
+
+type floatColumn struct {
+	Value float32
+}
+
+func (row floatColumn) generate(prng *rand.Rand) floatColumn {
+	return floatColumn{Value: prng.Float32()}
+}
+
+type doubleColumn struct {
+	Value float64
+}
+
+func (row doubleColumn) generate(prng *rand.Rand) doubleColumn {
+	return doubleColumn{Value: prng.Float64()}
+}
+
+type byteArrayColumn struct {
+	Value []byte
+}
+
+func (row byteArrayColumn) generate(prng *rand.Rand) byteArrayColumn {
+	row.Value = make([]byte, prng.Intn(10))
+	prng.Read(row.Value)
+	return row
+}
+
+type fixedLenByteArrayColumn struct {
+	Value [10]byte
+}
+
+func (row fixedLenByteArrayColumn) generate(prng *rand.Rand) fixedLenByteArrayColumn {
+	prng.Read(row.Value[:])
+	return row
+}
+
+type stringColumn struct {
+	Value string
+}
+
+func (row stringColumn) generate(prng *rand.Rand) stringColumn {
+	return stringColumn{Value: generateString(prng, 10)}
+}
+
+type indexedStringColumn struct {
+	Value string `parquet:",dict"`
+}
+
+func (row indexedStringColumn) generate(prng *rand.Rand) indexedStringColumn {
+	return indexedStringColumn{Value: generateString(prng, 10)}
+}
+
+type uuidColumn struct {
+	Value uuid.UUID `parquet:",delta"`
+}
+
+func (row uuidColumn) generate(prng *rand.Rand) uuidColumn {
+	prng.Read(row.Value[:])
+	return row
+}
+
+type decimalColumn struct {
+	Value int64 `parquet:",decimal(0:3)"`
+}
+
+func (row decimalColumn) generate(prng *rand.Rand) decimalColumn {
+	return decimalColumn{Value: prng.Int63()}
+}
+
+type mapColumn struct {
+	Value map[utf8string]int
+}
+
+func (row mapColumn) generate(prng *rand.Rand) mapColumn {
+	n := prng.Intn(10)
+	row.Value = make(map[utf8string]int, n)
+	for i := 0; i < n; i++ {
+		row.Value[utf8string(generateString(prng, 8))] = prng.Intn(100)
+	}
+	return row
+}
+
+type addressBook struct {
+	Owner             utf8string   `parquet:",plain"`
+	OwnerPhoneNumbers []utf8string `parquet:",plain"`
+	Contacts          []contact
+}
+
+type contact struct {
+	Name        utf8string `parquet:",plain"`
+	PhoneNumber utf8string `parquet:",plain"`
+}
+
+func (row contact) generate(prng *rand.Rand) contact {
+	return contact{
+		Name:        utf8string(generateString(prng, 16)),
+		PhoneNumber: utf8string(generateString(prng, 10)),
+	}
+}
+
+type listColumn2 struct {
+	Value utf8string `parquet:",optional"`
+}
+
+type listColumn1 struct {
+	List2 []listColumn2 `parquet:",list"`
+}
+
+type listColumn0 struct {
+	List1 []listColumn1 `parquet:",list"`
+}
+
+type nestedListColumn1 struct {
+	Level3 []utf8string `parquet:"level3"`
+}
+
+type nestedListColumn struct {
+	Level1 []nestedListColumn1 `parquet:"level1"`
+	Level2 []utf8string        `parquet:"level2"`
+}
+
+type utf8string string
+
+func (utf8string) Generate(rand *rand.Rand, size int) reflect.Value {
+	const characters = "abcdefghijklmnopqrstuvwxyz1234567890"
+	const maxSize = 10
+	if size > maxSize {
+		size = maxSize
+	}
+	n := rand.Intn(size)
+	b := make([]byte, n)
+	for i := range b {
+		b[i] = characters[rand.Intn(len(characters))]
+	}
+	return reflect.ValueOf(utf8string(b))
+}
+
 type Contact struct {
 	Name        string `parquet:"name"`
 	PhoneNumber string `parquet:"phoneNumber,optional,zstd"`
@@ -204,3 +395,26 @@ func copyRowsAndClose(w parquet.RowWriter, r parquet.Rows) (int64, error) {
 	defer r.Close()
 	return parquet.CopyRows(w, r)
 }
+
+func benchmarkRowsPerSecond(b *testing.B, f func() int) {
+	b.ResetTimer()
+	start := time.Now()
+	numRows := int64(0)
+
+	for i := 0; i < b.N; i++ {
+		n := f()
+		numRows += int64(n)
+	}
+
+	seconds := time.Since(start).Seconds()
+	b.ReportMetric(float64(numRows)/seconds, "row/s")
+}
+
+func generateString(r *rand.Rand, n int) string {
+	const characters = "1234567890qwertyuiopasdfghjklzxcvbnm"
+	b := new(strings.Builder)
+	for i := 0; i < n; i++ {
+		b.WriteByte(characters[r.Intn(len(characters))])
+	}
+	return b.String()
+}
diff --git a/reader_go18_test.go b/reader_go18_test.go
new file mode 100644
index 0000000..a48e927
--- /dev/null
+++ b/reader_go18_test.go
@@ -0,0 +1,149 @@
+//go:build go1.18
+
+package parquet_test
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"math/rand"
+	"reflect"
+	"testing"
+	"testing/quick"
+
+	"github.com/segmentio/parquet-go"
+)
+
+func TestGenericReader(t *testing.T) {
+	testGenericReader[booleanColumn](t)
+	testGenericReader[int32Column](t)
+	testGenericReader[int64Column](t)
+	testGenericReader[int96Column](t)
+	testGenericReader[floatColumn](t)
+	testGenericReader[doubleColumn](t)
+	testGenericReader[byteArrayColumn](t)
+	testGenericReader[fixedLenByteArrayColumn](t)
+	testGenericReader[stringColumn](t)
+	testGenericReader[indexedStringColumn](t)
+	testGenericReader[uuidColumn](t)
+	testGenericReader[mapColumn](t)
+	testGenericReader[decimalColumn](t)
+	testGenericReader[addressBook](t)
+	testGenericReader[contact](t)
+	testGenericReader[listColumn2](t)
+	testGenericReader[listColumn1](t)
+	testGenericReader[listColumn0](t)
+	testGenericReader[nestedListColumn1](t)
+	testGenericReader[nestedListColumn](t)
+	testGenericReader[*contact](t)
+}
+
+func testGenericReader[Row any](t *testing.T) {
+	var prng = rand.New(rand.NewSource(2))
+	var model Row
+	t.Run(reflect.TypeOf(model).Name(), func(t *testing.T) {
+		f := func(rows []Row) bool {
+			if len(rows) == 0 {
+				return true // TODO: fix support for parquet files with zero rows
+			}
+			if err := testGenericReaderRows(rows); err != nil {
+				t.Error(err)
+				return false
+			}
+			return true
+		}
+		if err := quick.Check(f, &quick.Config{Rand: prng}); err != nil {
+			t.Error(err)
+		}
+	})
+}
+
+func testGenericReaderRows[Row any](rows []Row) error {
+	setNullPointers(rows)
+	buffer := new(bytes.Buffer)
+	writer := parquet.NewGenericWriter[Row](buffer)
+	_, err := writer.Write(rows)
+	if err != nil {
+		return err
+	}
+	if err := writer.Close(); err != nil {
+		return err
+	}
+	reader := parquet.NewGenericReader[Row](bytes.NewReader(buffer.Bytes()))
+	result := make([]Row, len(rows))
+	n, err := reader.Read(result)
+	if err != nil && !errors.Is(err, io.EOF) {
+		return err
+	}
+	if n < len(rows) {
+		return fmt.Errorf("not enough values were read: want=%d got=%d", len(rows), n)
+	}
+	if !reflect.DeepEqual(rows, result) {
+		return fmt.Errorf("rows mismatch:\nwant: %+v\ngot:  %+v", rows, result)
+	}
+	return nil
+}
+
+func BenchmarkGenericReader(b *testing.B) {
+	benchmarkGenericReader[benchmarkRowType](b)
+	benchmarkGenericReader[booleanColumn](b)
+	benchmarkGenericReader[int32Column](b)
+	benchmarkGenericReader[int64Column](b)
+	benchmarkGenericReader[floatColumn](b)
+	benchmarkGenericReader[doubleColumn](b)
+	benchmarkGenericReader[byteArrayColumn](b)
+	benchmarkGenericReader[fixedLenByteArrayColumn](b)
+	benchmarkGenericReader[stringColumn](b)
+	benchmarkGenericReader[indexedStringColumn](b)
+	benchmarkGenericReader[uuidColumn](b)
+	benchmarkGenericReader[mapColumn](b)
+	benchmarkGenericReader[decimalColumn](b)
+	benchmarkGenericReader[contact](b)
+}
+
+func benchmarkGenericReader[Row generator[Row]](b *testing.B) {
+	var model Row
+	b.Run(reflect.TypeOf(model).Name(), func(b *testing.B) {
+		prng := rand.New(rand.NewSource(0))
+		rows := make([]Row, benchmarkNumRows)
+		for i := range rows {
+			rows[i] = rows[i].generate(prng)
+		}
+
+		rowbuf := make([]Row, benchmarkRowsPerStep)
+		buffer := parquet.NewGenericBuffer[Row]()
+		buffer.Write(rows)
+
+		b.Run("go1.17", func(b *testing.B) {
+			reader := parquet.NewRowGroupReader(buffer)
+			benchmarkRowsPerSecond(b, func() int {
+				for i := range rowbuf {
+					if err := reader.Read(&rowbuf[i]); err != nil {
+						if err != io.EOF {
+							b.Fatal(err)
+						} else {
+							reader.Reset()
+						}
+					}
+				}
+				return len(rowbuf)
+			})
+		})
+
+		b.Run("go1.18", func(b *testing.B) {
+			reader := parquet.NewGenericRowGroupReader[Row](buffer)
+			benchmarkRowsPerSecond(b, func() int {
+				n, err := reader.Read(rowbuf)
+				if err != nil {
+					if err != io.EOF {
+						b.Fatal(err)
+					} else {
+						reader.Reset()
+					}
+				}
+				return n
+			})
+		})
+	})
+}
diff --git a/reader_test.go b/reader_test.go
index 04208bc..9449e52 100644
--- a/reader_test.go
+++ b/reader_test.go
@@ -9,109 +9,10 @@ import (
 	"reflect"
 	"testing"
 	"testing/quick"
-	"time"
 
-	"github.com/google/uuid"
 	"github.com/segmentio/parquet-go"
-	"github.com/segmentio/parquet-go/deprecated"
 )
 
-type booleanColumn struct {
-	Value bool
-}
-
-type int32Column struct {
-	Value int32
-}
-
-type int64Column struct {
-	Value int64
-}
-
-type int96Column struct {
-	Value deprecated.Int96
-}
-
-type floatColumn struct {
-	Value float32
-}
-
-type doubleColumn struct {
-	Value float64
-}
-
-type byteArrayColumn struct {
-	Value []byte
-}
-
-type fixedLenByteArrayColumn struct {
-	Value [10]byte
-}
-
-type stringColumn struct {
-	Value string
-}
-
-type indexedStringColumn struct {
-	Value string `parquet:",dict"`
-}
-
-type uuidColumn struct {
-	Value uuid.UUID `parquet:",delta"`
-}
-
-type decimalColumn struct {
-	Value int64 `parquet:",decimal(0:3)"`
-}
-
-type addressBook struct {
-	Owner             utf8string   `parquet:",plain"`
-	OwnerPhoneNumbers []utf8string `parquet:",plain"`
-	Contacts          []contact
-}
-
-type contact struct {
-	Name        utf8string `parquet:",plain"`
-	PhoneNumber utf8string `parquet:",plain"`
-}
-
-type listColumn2 struct {
-	Value utf8string `parquet:",optional"`
-}
-
-type listColumn1 struct {
-	List2 []listColumn2 `parquet:",list"`
-}
-
-type listColumn0 struct {
-	List1 []listColumn1 `parquet:",list"`
-}
-
-type nestedListColumn1 struct {
-	Level3 []utf8string `parquet:"level3"`
-}
-
-type nestedListColumn struct {
-	Level1 []nestedListColumn1 `parquet:"level1"`
-	Level2 []utf8string        `parquet:"level2"`
-}
-
-type utf8string string
-
-func (utf8string) Generate(rand *rand.Rand, size int) reflect.Value {
-	const characters = "abcdefghijklmnopqrstuvwxyz1234567890"
-	const maxSize = 10
-	if size > maxSize {
-		size = maxSize
-	}
-	n := rand.Intn(size)
-	b := make([]byte, n)
-	for i := range b {
-		b[i] = characters[rand.Intn(len(characters))]
-	}
-	return reflect.ValueOf(utf8string(b))
-}
-
 func rowsOf(numRows int, model interface{}) rows {
 	prng := rand.New(rand.NewSource(0))
 	return randomRowsOf(prng, numRows, model)
@@ -299,10 +200,6 @@ func TestReader(t *testing.T) {
 	}
 }
 
-const (
-	benchmarkReaderNumRows = 10e3
-)
-
 func BenchmarkReaderReadType(b *testing.B) {
 	buf := new(bytes.Buffer)
 	file := bytes.NewReader(nil)
@@ -310,7 +207,7 @@ func BenchmarkReaderReadType(b *testing.B) {
 	for _, test := range readerTests {
 		b.Run(test.scenario, func(b *testing.B) {
 			defer buf.Reset()
-			rows := rowsOf(benchmarkReaderNumRows, test.model)
+			rows := rowsOf(benchmarkNumRows, test.model)
 
 			if err := writeParquetFile(buf, rows); err != nil {
 				b.Fatal(err)
@@ -329,23 +226,21 @@ func BenchmarkReaderReadType(b *testing.B) {
 			r := parquet.NewReader(f)
 			p := rowPtr.Interface()
 
-			b.ResetTimer()
-			start := time.Now()
-
-			for i := 0; i < b.N; i++ {
-				if err := r.Read(p); err != nil {
-					if err == io.EOF {
-						r.Reset()
-					} else {
-						b.Fatalf("%d/%d: %v", i, b.N, err)
+			benchmarkRowsPerSecond(b, func() (n int) {
+				for i := 0; i < benchmarkRowsPerStep; i++ {
+					if err := r.Read(p); err != nil {
+						if err == io.EOF {
+							r.Reset()
+						} else {
+							b.Fatal(err)
+						}
 					}
 				}
 				rowValue.Set(rowZero)
-			}
+				return benchmarkRowsPerStep
+			})
 
-			seconds := time.Since(start).Seconds()
-			b.ReportMetric(float64(b.N)/seconds, "row/s")
-			b.SetBytes(int64(math.Ceil(float64(file.Size()) / benchmarkReaderNumRows)))
+			b.SetBytes(int64(math.Ceil(benchmarkRowsPerStep * float64(file.Size()) / benchmarkNumRows)))
 		})
 	}
 }
@@ -357,7 +252,7 @@ func BenchmarkReaderReadRow(b *testing.B) {
 	for _, test := range readerTests {
 		b.Run(test.scenario, func(b *testing.B) {
 			defer buf.Reset()
-			rows := rowsOf(benchmarkReaderNumRows, test.model)
+			rows := rowsOf(benchmarkNumRows, test.model)
 
 			if err := writeParquetFile(buf, rows); err != nil {
 				b.Fatal(err)
@@ -369,25 +264,21 @@ func BenchmarkReaderReadRow(b *testing.B) {
 			}
 
 			r := parquet.NewReader(f)
-			rowbuf := make([]parquet.Row, 0, 16)
-
-			b.ResetTimer()
-			start := time.Now()
+			rowbuf := make([]parquet.Row, benchmarkRowsPerStep)
 
-			for i := 0; i < b.N; i++ {
-				_, err := r.ReadRows(rowbuf)
+			benchmarkRowsPerSecond(b, func() int {
+				n, err := r.ReadRows(rowbuf)
 				if err != nil {
 					if err == io.EOF {
 						r.Reset()
 					} else {
-						b.Fatalf("%d/%d: %v", i, b.N, err)
+						b.Fatal(err)
 					}
 				}
-			}
+				return n
+			})
 
-			seconds := time.Since(start).Seconds()
-			b.ReportMetric(float64(b.N)/seconds, "row/s")
-			b.SetBytes(int64(math.Ceil(float64(file.Size()) / benchmarkReaderNumRows)))
+			b.SetBytes(int64(math.Ceil(benchmarkRowsPerStep * float64(file.Size()) / benchmarkNumRows)))
 		})
 	}
 }
diff --git a/writer_go18_test.go b/writer_go18_test.go
new file mode 100644
index 0000000..8893162
--- /dev/null
+++ b/writer_go18_test.go
@@ -0,0 +1,81 @@
+//go:build go1.18
+
+package parquet_test
+
+import (
+	"io"
+	"math/rand"
+	"reflect"
+	"testing"
+
+	"github.com/segmentio/parquet-go"
+)
+
+func BenchmarkGenericWriter(b *testing.B) {
+	benchmarkGenericWriter[benchmarkRowType](b)
+	benchmarkGenericWriter[booleanColumn](b)
+	benchmarkGenericWriter[int32Column](b)
+	benchmarkGenericWriter[int64Column](b)
+	benchmarkGenericWriter[floatColumn](b)
+	benchmarkGenericWriter[doubleColumn](b)
+	benchmarkGenericWriter[byteArrayColumn](b)
+	benchmarkGenericWriter[fixedLenByteArrayColumn](b)
+	benchmarkGenericWriter[stringColumn](b)
+	benchmarkGenericWriter[indexedStringColumn](b)
+	benchmarkGenericWriter[uuidColumn](b)
+	benchmarkGenericWriter[mapColumn](b)
+	benchmarkGenericWriter[decimalColumn](b)
+	benchmarkGenericWriter[contact](b)
+}
+
+func benchmarkGenericWriter[Row generator[Row]](b *testing.B) {
+	var model Row
+	b.Run(reflect.TypeOf(model).Name(), func(b *testing.B) {
+		prng := rand.New(rand.NewSource(0))
+		rows := make([]Row, benchmarkNumRows)
+		for i := range rows {
+			rows[i] = rows[i].generate(prng)
+		}
+
+		b.Run("go1.17", func(b *testing.B) {
+			writer := parquet.NewWriter(io.Discard, parquet.SchemaOf(rows[0]))
+			i := 0
+			benchmarkRowsPerSecond(b, func() int {
+				for j := 0; j < benchmarkRowsPerStep; j++ {
+					if err := writer.Write(&rows[i]); err != nil {
+						b.Fatal(err)
+					}
+				}
+
+				i += benchmarkRowsPerStep
+				i %= benchmarkNumRows
+
+				if i == 0 {
+					writer.Close()
+					writer.Reset(io.Discard)
+				}
+				return benchmarkRowsPerStep
+			})
+		})
+
+		b.Run("go1.18", func(b *testing.B) {
+			writer := parquet.NewGenericWriter[Row](io.Discard)
+			i := 0
+			benchmarkRowsPerSecond(b, func() int {
+				n, err := writer.Write(rows[i : i+benchmarkRowsPerStep])
+				if err != nil {
+					b.Fatal(err)
+				}
+
+				i += benchmarkRowsPerStep
+				i %= benchmarkNumRows
+
+				if i == 0 {
+					writer.Close()
+					writer.Reset(io.Discard)
+				}
+				return n
+			})
+		})
+	})
+}