Repository: cilium/ebpf
Pull request #1763 (2025-04-23 17:10:01)

btf: lazy decoding
Prompted by #1755 another stab at lazy decoding of BTF types. IterateVmlinux is roughly the pwru workload (IIRC) and InspectorGadget does what it says on the tin.

    core: 1
    goos: linux
    goarch: amd64
    pkg: github.com/cilium/ebpf/btf
    cpu: 13th Gen Intel(R) Core(TM) i7-1365U
                    │  base.txt   │          eager-strings.txt           │
                    │   sec/op    │   sec/op     vs base                 │
    ParseVmlinux      80.59m ± 4%   26.95m ± 2%  -66.56% (p=0.002 n=6)
    IterateVmlinux    193.7m ± 5%   151.8m ± 2%  -21.65% (p=0.002 n=6)
    InspectorGadget   87.22m ± 1%   34.56m ± 2%  -60.37% (p=0.002 n=6)
    BuildVmlinux      145.7m ± 4%
    geomean           118.7m        52.09m       -53.00%               ¹
    ¹ benchmark set differs from baseline; geomeans may not be comparable
    
                    │   base.txt    │           eager-strings.txt           │
                    │     B/op      │     B/op      vs base                 │
    ParseVmlinux      24.427Mi ± 0%   9.970Mi ± 0%  -59.18% (p=0.002 n=6)
    IterateVmlinux     52.51Mi ± 0%   34.71Mi ± 0%  -33.90% (p=0.002 n=6)
    InspectorGadget    26.65Mi ± 0%   12.00Mi ± 0%  -54.97% (p=0.002 n=6)
    BuildVmlinux       42.22Mi ± 0%
    geomean            34.66Mi        16.07Mi       -50.47%               ¹
    ¹ benchmark set differs from baseline; geomeans may not be comparable
    
                    │  base.txt   │          eager-strings.txt           │
                    │  allocs/op  │  allocs/op   vs base                 │
    ParseVmlinux      271.9k ± 0%   146.1k ± 0%  -46.27% (p=0.002 n=6)
    IterateVmlinux    398.9k ± 0%   272.9k ± 0%  -31.60% (p=0.002 n=6)
    InspectorGadget   280.8k ± 0%   155.0k ± 0%  -44.80% (p=0.002 n=6)
    BuildVmlinux      1.508k ± 0%
    geomean           82.32k        183.5k       -41.24%               ¹
    ¹ benchmark set differs from baseline; geomeans may not be comparable

lmb: @brb could you try this in pwru and see how bad things are?
brb: @lmb Are you interested in perf benchmarks, or correctness? If the latter, then all good - https://github.com/cilium/pwru/pull/548. Unfortunately, we do not have any benchmarking suites.
lmb: I added a crude benchmark which just iterates all of vmlinux to simulate pwru. Even that has gotten faster!

Patch:
diff --git a/btf/btf.go b/btf/btf.go
index 2f9b211..4bf0f3d 100644
--- a/btf/btf.go
+++ b/btf/btf.go
@@ -7,10 +7,11 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"iter"
+	"maps"
 	"math"
 	"os"
 	"reflect"
-	"sync"
 
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/sys"
@@ -29,154 +30,24 @@ var (
 // ID represents the unique ID of a BTF object.
 type ID = sys.BTFID
 
-// immutableTypes is a set of types which musn't be changed.
-type immutableTypes struct {
-	// All types contained by the spec, not including types from the base in
-	// case the spec was parsed from split BTF.
-	types []Type
-
-	// Type IDs indexed by type.
-	typeIDs map[Type]TypeID
-
-	// The ID of the first type in types.
-	firstTypeID TypeID
-
-	// Types indexed by essential name.
-	// Includes all struct flavors and types with the same name.
-	namedTypes map[essentialName][]TypeID
-
-	// Byte order of the types. This affects things like struct member order
-	// when using bitfields.
-	byteOrder binary.ByteOrder
-}
-
-func (s *immutableTypes) typeByID(id TypeID) (Type, bool) {
-	if id < s.firstTypeID {
-		return nil, false
-	}
-
-	index := int(id - s.firstTypeID)
-	if index >= len(s.types) {
-		return nil, false
-	}
-
-	return s.types[index], true
-}
-
-// mutableTypes is a set of types which may be changed.
-type mutableTypes struct {
-	imm           immutableTypes
-	mu            sync.RWMutex    // protects copies below
-	copies        map[Type]Type   // map[orig]copy
-	copiedTypeIDs map[Type]TypeID // map[copy]origID
-}
-
-// add a type to the set of mutable types.
-//
-// Copies type and all of its children once. Repeated calls with the same type
-// do not copy again.
-func (mt *mutableTypes) add(typ Type, typeIDs map[Type]TypeID) Type {
-	mt.mu.RLock()
-	cpy, ok := mt.copies[typ]
-	mt.mu.RUnlock()
-
-	if ok {
-		// Fast path: the type has been copied before.
-		return cpy
-	}
-
-	// modifyGraphPreorder copies the type graph node by node, so we can't drop
-	// the lock in between.
-	mt.mu.Lock()
-	defer mt.mu.Unlock()
-
-	return copyType(typ, typeIDs, mt.copies, mt.copiedTypeIDs)
-}
-
-// copy a set of mutable types.
-func (mt *mutableTypes) copy() *mutableTypes {
-	if mt == nil {
-		return nil
-	}
-
-	// Prevent concurrent modification of mt.copiedTypeIDs.
-	mt.mu.RLock()
-	defer mt.mu.RUnlock()
-
-	mtCopy := &mutableTypes{
-		mt.imm,
-		sync.RWMutex{},
-		make(map[Type]Type, len(mt.copies)),
-		make(map[Type]TypeID, len(mt.copiedTypeIDs)),
-	}
-
-	copiesOfCopies := make(map[Type]Type, len(mt.copies))
-	for orig, copy := range mt.copies {
-		// NB: We make a copy of copy, not orig, so that changes to mutable types
-		// are preserved.
-		copyOfCopy := copyType(copy, mt.copiedTypeIDs, copiesOfCopies, mtCopy.copiedTypeIDs)
-		mtCopy.copies[orig] = copyOfCopy
-	}
-
-	return mtCopy
-}
-
-func (mt *mutableTypes) typeID(typ Type) (TypeID, error) {
-	if _, ok := typ.(*Void); ok {
-		// Equality is weird for void, since it is a zero sized type.
-		return 0, nil
-	}
-
-	mt.mu.RLock()
-	defer mt.mu.RUnlock()
-
-	id, ok := mt.copiedTypeIDs[typ]
-	if !ok {
-		return 0, fmt.Errorf("no ID for type %s: %w", typ, ErrNotFound)
-	}
-
-	return id, nil
-}
-
-func (mt *mutableTypes) typeByID(id TypeID) (Type, bool) {
-	immT, ok := mt.imm.typeByID(id)
-	if !ok {
-		return nil, false
-	}
-
-	return mt.add(immT, mt.imm.typeIDs), true
+type elfData struct {
+	sectionSizes  map[string]uint32
+	symbolOffsets map[elfSymbol]uint32
+	fixups        map[Type]bool
 }
 
-func (mt *mutableTypes) anyTypesByName(name string) ([]Type, error) {
-	immTypes := mt.imm.namedTypes[newEssentialName(name)]
-	if len(immTypes) == 0 {
-		return nil, fmt.Errorf("type name %s: %w", name, ErrNotFound)
-	}
-
-	// Return a copy to prevent changes to namedTypes.
-	result := make([]Type, 0, len(immTypes))
-	for _, id := range immTypes {
-		immT, ok := mt.imm.typeByID(id)
-		if !ok {
-			return nil, fmt.Errorf("no type with ID %d", id)
-		}
-
-		// Match against the full name, not just the essential one
-		// in case the type being looked up is a struct flavor.
-		if immT.TypeName() == name {
-			result = append(result, mt.add(immT, mt.imm.typeIDs))
-		}
-	}
-	return result, nil
+type elfSymbol struct {
+	section string
+	name    string
 }
 
 // Spec allows querying a set of Types and loading the set into the
 // kernel.
 type Spec struct {
-	*mutableTypes
+	*decoder
 
-	// String table from ELF.
-	strings *stringTable
+	// Additional data from ELF, may be nil.
+	elf *elfData
 }
 
 // LoadSpec opens file and calls LoadSpecFromReader on it.
@@ -237,13 +108,13 @@ func LoadSpecAndExtInfosFromReader(rd io.ReaderAt) (*Spec, *ExtInfos, error) {
 // Some ELF symbols (e.g. in vmlinux) may point to virtual memory that is well
 // beyond this range. Since these symbols cannot be described by BTF info,
 // ignore them here.
-func symbolOffsets(file *internal.SafeELFFile) (map[symbol]uint32, error) {
+func symbolOffsets(file *internal.SafeELFFile) (map[elfSymbol]uint32, error) {
 	symbols, err := file.Symbols()
 	if err != nil {
 		return nil, fmt.Errorf("can't read symbols: %v", err)
 	}
 
-	offsets := make(map[symbol]uint32)
+	offsets := make(map[elfSymbol]uint32)
 	for _, sym := range symbols {
 		if idx := sym.Section; idx >= elf.SHN_LORESERVE && idx <= elf.SHN_HIRESERVE {
 			// Ignore things like SHN_ABS
@@ -260,7 +131,7 @@ func symbolOffsets(file *internal.SafeELFFile) (map[symbol]uint32, error) {
 		}
 
 		secName := file.Sections[sym.Section].Name
-		offsets[symbol{secName, sym.Name}] = uint32(sym.Value)
+		offsets[elfSymbol{secName, sym.Name}] = uint32(sym.Value)
 	}
 
 	return offsets, nil
@@ -307,9 +178,10 @@ func loadSpecFromELF(file *internal.SafeELFFile) (*Spec, error) {
 		return nil, err
 	}
 
-	err = fixupDatasec(spec.imm.types, sectionSizes, offsets)
-	if err != nil {
-		return nil, err
+	spec.elf = &elfData{
+		sectionSizes,
+		offsets,
+		make(map[Type]bool),
 	}
 
 	return spec, nil
@@ -317,72 +189,40 @@ func loadSpecFromELF(file *internal.SafeELFFile) (*Spec, error) {
 
 func loadRawSpec(btf io.ReaderAt, bo binary.ByteOrder, base *Spec) (*Spec, error) {
 	var (
+		baseDecoder *decoder
 		baseStrings *stringTable
-		firstTypeID TypeID
 		err         error
 	)
 
 	if base != nil {
-		if base.imm.firstTypeID != 0 {
-			return nil, fmt.Errorf("can't use split BTF as base")
-		}
-
+		baseDecoder = base.decoder
 		baseStrings = base.strings
-
-		firstTypeID, err = base.nextTypeID()
-		if err != nil {
-			return nil, err
-		}
 	}
 
-	types, rawStrings, err := parseBTF(btf, bo, baseStrings, base)
+	buf := internal.NewBufferedSectionReader(btf, 0, math.MaxInt64)
+	header, err := parseBTFHeader(buf, bo)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("parsing .BTF header: %v", err)
 	}
 
-	typeIDs, typesByName := indexTypes(types, firstTypeID)
-
-	return &Spec{
-		&mutableTypes{
-			immutableTypes{
-				types,
-				typeIDs,
-				firstTypeID,
-				typesByName,
-				bo,
-			},
-			sync.RWMutex{},
-			make(map[Type]Type),
-			make(map[Type]TypeID),
-		},
-		rawStrings,
-	}, nil
-}
-
-func indexTypes(types []Type, firstTypeID TypeID) (map[Type]TypeID, map[essentialName][]TypeID) {
-	namedTypes := 0
-	for _, typ := range types {
-		if typ.TypeName() != "" {
-			// Do a pre-pass to figure out how big types by name has to be.
-			// Most types have unique names, so it's OK to ignore essentialName
-			// here.
-			namedTypes++
-		}
+	stringsSection := io.NewSectionReader(btf, header.stringStart(), int64(header.StringLen))
+	rawStrings, err := readStringTable(stringsSection, baseStrings)
+	if err != nil {
+		return nil, fmt.Errorf("read string section: %w", err)
 	}
 
-	typeIDs := make(map[Type]TypeID, len(types))
-	typesByName := make(map[essentialName][]TypeID, namedTypes)
-
-	for i, typ := range types {
-		id := firstTypeID + TypeID(i)
-		typeIDs[typ] = id
+	typesSection := io.NewSectionReader(btf, header.typeStart(), int64(header.TypeLen))
+	rawTypes := make([]byte, header.TypeLen)
+	if _, err := io.ReadFull(typesSection, rawTypes); err != nil {
+		return nil, fmt.Errorf("read type section: %w", err)
+	}
 
-		if name := newEssentialName(typ.TypeName()); name != "" {
-			typesByName[name] = append(typesByName[name], id)
-		}
+	decoder, err := newDecoder(rawTypes, bo, rawStrings, baseDecoder)
+	if err != nil {
+		return nil, err
 	}
 
-	return typeIDs, typesByName
+	return &Spec{decoder, nil}, nil
 }
 
 func guessRawBTFByteOrder(r io.ReaderAt) binary.ByteOrder {
@@ -400,43 +240,14 @@ func guessRawBTFByteOrder(r io.ReaderAt) binary.ByteOrder {
 	return nil
 }
 
-// parseBTF reads a .BTF section into memory and parses it into a list of
-// raw types and a string table.
-func parseBTF(btf io.ReaderAt, bo binary.ByteOrder, baseStrings *stringTable, base *Spec) ([]Type, *stringTable, error) {
-	buf := internal.NewBufferedSectionReader(btf, 0, math.MaxInt64)
-	header, err := parseBTFHeader(buf, bo)
-	if err != nil {
-		return nil, nil, fmt.Errorf("parsing .BTF header: %v", err)
-	}
-
-	rawStrings, err := readStringTable(io.NewSectionReader(btf, header.stringStart(), int64(header.StringLen)),
-		baseStrings)
-	if err != nil {
-		return nil, nil, fmt.Errorf("can't read type names: %w", err)
-	}
-
-	buf.Reset(io.NewSectionReader(btf, header.typeStart(), int64(header.TypeLen)))
-	types, err := readAndInflateTypes(buf, bo, header.TypeLen, rawStrings, base)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	return types, rawStrings, nil
-}
-
-type symbol struct {
-	section string
-	name    string
-}
-
 // fixupDatasec attempts to patch up missing info in Datasecs and its members by
 // supplementing them with information from the ELF headers and symbol table.
-func fixupDatasec(types []Type, sectionSizes map[string]uint32, offsets map[symbol]uint32) error {
-	for _, typ := range types {
-		ds, ok := typ.(*Datasec)
-		if !ok {
-			continue
+func (elf *elfData) fixupDatasec(typ Type) error {
+	if ds, ok := typ.(*Datasec); ok {
+		if elf.fixups[ds] {
+			return nil
 		}
+		elf.fixups[ds] = true
 
 		name := ds.Name
 
@@ -459,7 +270,7 @@ func fixupDatasec(types []Type, sectionSizes map[string]uint32, offsets map[symb
 				}
 			}
 
-			continue
+			return nil
 		case ".kconfig":
 			// .kconfig has a size of 0 and has all members' offsets set to 0.
 			// Fix up all offsets and set the Datasec's size.
@@ -472,21 +283,21 @@ func fixupDatasec(types []Type, sectionSizes map[string]uint32, offsets map[symb
 				vsi.Type.(*Var).Linkage = GlobalVar
 			}
 
-			continue
+			return nil
 		}
 
 		if ds.Size != 0 {
-			continue
+			return nil
 		}
 
-		ds.Size, ok = sectionSizes[name]
+		ds.Size, ok = elf.sectionSizes[name]
 		if !ok {
 			return fmt.Errorf("data section %s: missing size", name)
 		}
 
 		for i := range ds.Vars {
 			symName := ds.Vars[i].Type.TypeName()
-			ds.Vars[i].Offset, ok = offsets[symbol{name, symName}]
+			ds.Vars[i].Offset, ok = elf.symbolOffsets[elfSymbol{name, symName}]
 			if !ok {
 				return fmt.Errorf("data section %s: missing offset for symbol %s", name, symName)
 			}
@@ -536,20 +347,20 @@ func (s *Spec) Copy() *Spec {
 		return nil
 	}
 
-	return &Spec{
-		s.copy(),
-		s.strings,
+	cpy := &Spec{
+		s.decoder.Copy(),
+		nil,
 	}
-}
 
-// nextTypeID returns the next unallocated type ID or an error if there are no
-// more type IDs.
-func (s *Spec) nextTypeID() (TypeID, error) {
-	id := s.imm.firstTypeID + TypeID(len(s.imm.types))
-	if id < s.imm.firstTypeID {
-		return 0, fmt.Errorf("no more type IDs")
+	if s.elf != nil {
+		cpy.elf = &elfData{
+			s.elf.sectionSizes,
+			s.elf.symbolOffsets,
+			maps.Clone(s.elf.fixups),
+		}
 	}
-	return id, nil
+
+	return cpy
 }
 
 // TypeByID returns the BTF Type with the given type ID.
@@ -557,9 +368,17 @@ func (s *Spec) nextTypeID() (TypeID, error) {
 // Returns an error wrapping ErrNotFound if a Type with the given ID
 // does not exist in the Spec.
 func (s *Spec) TypeByID(id TypeID) (Type, error) {
-	typ, ok := s.typeByID(id)
-	if !ok {
-		return nil, fmt.Errorf("look up type with ID %d (first ID is %d): %w", id, s.imm.firstTypeID, ErrNotFound)
+	typ, err := s.decoder.TypeByID(id)
+	if err != nil {
+		return nil, fmt.Errorf("inflate type: %w", err)
+	}
+
+	if s.elf == nil {
+		return typ, nil
+	}
+
+	if err := s.elf.fixupDatasec(typ); err != nil {
+		return nil, err
 	}
 
 	return typ, nil
@@ -569,7 +388,7 @@ func (s *Spec) TypeByID(id TypeID) (Type, error) {
 //
 // Returns an error wrapping [ErrNotFound] if the type isn't part of the Spec.
 func (s *Spec) TypeID(typ Type) (TypeID, error) {
-	return s.typeID(typ)
+	return s.decoder.TypeID(typ)
 }
 
 // AnyTypesByName returns a list of BTF Types with the given name.
@@ -580,7 +399,28 @@ func (s *Spec) TypeID(typ Type) (TypeID, error) {
 //
 // Returns an error wrapping ErrNotFound if no matching Type exists in the Spec.
 func (s *Spec) AnyTypesByName(name string) ([]Type, error) {
-	return s.anyTypesByName(name)
+	typeIDs := s.TypeIDsByName(newEssentialName(name))
+	if len(typeIDs) == 0 {
+		return nil, fmt.Errorf("type name %s: %w", name, ErrNotFound)
+	}
+
+	// Return a copy to prevent changes to namedTypes.
+	result := make([]Type, 0, len(typeIDs))
+	for _, id := range typeIDs {
+		typ, err := s.TypeByID(id)
+		if errors.Is(err, ErrNotFound) {
+			return nil, fmt.Errorf("no type with ID %d", id)
+		} else if err != nil {
+			return nil, err
+		}
+
+		// Match against the full name, not just the essential one
+		// in case the type being looked up is a struct flavor.
+		if typ.TypeName() == name {
+			result = append(result, typ)
+		}
+	}
+	return result, nil
 }
 
 // AnyTypeByName returns a Type with the given name.
@@ -667,37 +507,28 @@ func LoadSplitSpecFromReader(r io.ReaderAt, base *Spec) (*Spec, error) {
 	return loadRawSpec(r, internal.NativeEndian, base)
 }
 
-// TypesIterator iterates over types of a given spec.
-type TypesIterator struct {
-	spec *Spec
-	id   TypeID
-	done bool
-	// The last visited type in the spec.
-	Type Type
-}
+// All iterates over all types.
+func (s *Spec) All() iter.Seq2[Type, error] {
+	return func(yield func(Type, error) bool) {
+		for id := s.firstTypeID; ; id++ {
+			typ, err := s.TypeByID(id)
+			if errors.Is(err, ErrNotFound) {
+				return
+			} else if err != nil {
+				yield(nil, err)
+				return
+			}
 
-// Iterate returns the types iterator.
-func (s *Spec) Iterate() *TypesIterator {
-	return &TypesIterator{spec: s, id: s.imm.firstTypeID}
-}
+			// Skip declTags, during unmarshaling declTags become `Tags` fields of other types.
+			// We keep them in the spec to avoid holes in the ID space, but for the purposes of
+			// iteration, they are not useful to the user.
+			if _, ok := typ.(*declTag); ok {
+				continue
+			}
 
-// Next returns true as long as there are any remaining types.
-func (iter *TypesIterator) Next() bool {
-	if iter.done {
-		return false
-	}
-
-	var ok bool
-	iter.Type, ok = iter.spec.typeByID(iter.id)
-	iter.id++
-	iter.done = !ok
-	if !iter.done {
-		// Skip declTags, during unmarshaling declTags become `Tags` fields of other types.
-		// We keep them in the spec to avoid holes in the ID space, but for the purposes of
-		// iteration, they are not useful to the user.
-		if _, ok := iter.Type.(*declTag); ok {
-			return iter.Next()
+			if !yield(typ, nil) {
+				return
+			}
 		}
 	}
-	return !iter.done
 }
diff --git a/btf/btf_types.go b/btf/btf_types.go
index 95bd756..d20a319 100644
--- a/btf/btf_types.go
+++ b/btf/btf_types.go
@@ -130,8 +130,6 @@ func parseBTFHeader(r io.Reader, bo binary.ByteOrder) (*btfHeader, error) {
 	return &header, nil
 }
 
-var btfTypeLen = binary.Size(btfType{})
-
 // btfType is equivalent to struct btf_type in Documentation/bpf/btf.rst.
 type btfType struct {
 	NameOff uint32
@@ -278,6 +276,45 @@ func (bt *btfType) Encode(buf []byte, bo binary.ByteOrder) (int, error) {
 	return btfTypeSize, nil
 }
 
+// DataLen returns the length of additional type specific data in bytes.
+func (bt *btfType) DataLen() (int, error) {
+	switch bt.Kind() {
+	case kindInt:
+		return int(unsafe.Sizeof(btfInt{})), nil
+	case kindPointer:
+	case kindArray:
+		return int(unsafe.Sizeof(btfArray{})), nil
+	case kindStruct:
+		fallthrough
+	case kindUnion:
+		return int(unsafe.Sizeof(btfMember{})) * bt.Vlen(), nil
+	case kindEnum:
+		return int(unsafe.Sizeof(btfEnum{})) * bt.Vlen(), nil
+	case kindForward:
+	case kindTypedef:
+	case kindVolatile:
+	case kindConst:
+	case kindRestrict:
+	case kindFunc:
+	case kindFuncProto:
+		return int(unsafe.Sizeof(btfParam{})) * bt.Vlen(), nil
+	case kindVar:
+		return int(unsafe.Sizeof(btfVariable{})), nil
+	case kindDatasec:
+		return int(unsafe.Sizeof(btfVarSecinfo{})) * bt.Vlen(), nil
+	case kindFloat:
+	case kindDeclTag:
+		return int(unsafe.Sizeof(btfDeclTag{})), nil
+	case kindTypeTag:
+	case kindEnum64:
+		return int(unsafe.Sizeof(btfEnum64{})) * bt.Vlen(), nil
+	default:
+		return 0, fmt.Errorf("unknown kind: %v", bt.Kind())
+	}
+
+	return 0, nil
+}
+
 // btfInt encodes additional data for integers.
 //
 //	? ? ? ? e e e e o o o o o o o o ? ? ? ? ? ? ? ? b b b b b b b b
@@ -360,21 +397,15 @@ type btfMember struct {
 
 var btfMemberLen = int(unsafe.Sizeof(btfMember{}))
 
-func unmarshalBtfMembers(members []btfMember, b []byte, bo binary.ByteOrder) (int, error) {
-	off := 0
-	for i := range members {
-		if off+btfMemberLen > len(b) {
-			return 0, fmt.Errorf("not enough bytes to unmarshal btfMember %d", i)
-		}
-
-		members[i].NameOff = bo.Uint32(b[off+0:])
-		members[i].Type = TypeID(bo.Uint32(b[off+4:]))
-		members[i].Offset = bo.Uint32(b[off+8:])
-
-		off += btfMemberLen
+func unmarshalBtfMember(bm *btfMember, b []byte, bo binary.ByteOrder) (int, error) {
+	if btfMemberLen > len(b) {
+		return 0, fmt.Errorf("not enough bytes to unmarshal btfMember")
 	}
 
-	return off, nil
+	bm.NameOff = bo.Uint32(b[0:])
+	bm.Type = TypeID(bo.Uint32(b[4:]))
+	bm.Offset = bo.Uint32(b[8:])
+	return btfMemberLen, nil
 }
 
 type btfVarSecinfo struct {
@@ -385,21 +416,15 @@ type btfVarSecinfo struct {
 
 var btfVarSecinfoLen = int(unsafe.Sizeof(btfVarSecinfo{}))
 
-func unmarshalBtfVarSecInfos(secinfos []btfVarSecinfo, b []byte, bo binary.ByteOrder) (int, error) {
-	off := 0
-	for i := range secinfos {
-		if off+btfVarSecinfoLen > len(b) {
-			return 0, fmt.Errorf("not enough bytes to unmarshal btfVarSecinfo %d", i)
-		}
-
-		secinfos[i].Type = TypeID(bo.Uint32(b[off+0:]))
-		secinfos[i].Offset = bo.Uint32(b[off+4:])
-		secinfos[i].Size = bo.Uint32(b[off+8:])
-
-		off += btfVarSecinfoLen
+func unmarshalBtfVarSecInfo(bvsi *btfVarSecinfo, b []byte, bo binary.ByteOrder) (int, error) {
+	if len(b) < btfVarSecinfoLen {
+		return 0, fmt.Errorf("not enough bytes to unmarshal btfVarSecinfo")
 	}
 
-	return off, nil
+	bvsi.Type = TypeID(bo.Uint32(b[0:]))
+	bvsi.Offset = bo.Uint32(b[4:])
+	bvsi.Size = bo.Uint32(b[8:])
+	return btfVarSecinfoLen, nil
 }
 
 type btfVariable struct {
@@ -424,20 +449,14 @@ type btfEnum struct {
 
 var btfEnumLen = int(unsafe.Sizeof(btfEnum{}))
 
-func unmarshalBtfEnums(enums []btfEnum, b []byte, bo binary.ByteOrder) (int, error) {
-	off := 0
-	for i := range enums {
-		if off+btfEnumLen > len(b) {
-			return 0, fmt.Errorf("not enough bytes to unmarshal btfEnum %d", i)
-		}
-
-		enums[i].NameOff = bo.Uint32(b[off+0:])
-		enums[i].Val = bo.Uint32(b[off+4:])
-
-		off += btfEnumLen
+func unmarshalBtfEnum(be *btfEnum, b []byte, bo binary.ByteOrder) (int, error) {
+	if btfEnumLen > len(b) {
+		return 0, fmt.Errorf("not enough bytes to unmarshal btfEnum")
 	}
 
-	return off, nil
+	be.NameOff = bo.Uint32(b[0:])
+	be.Val = bo.Uint32(b[4:])
+	return btfEnumLen, nil
 }
 
 type btfEnum64 struct {
@@ -448,21 +467,16 @@ type btfEnum64 struct {
 
 var btfEnum64Len = int(unsafe.Sizeof(btfEnum64{}))
 
-func unmarshalBtfEnums64(enums []btfEnum64, b []byte, bo binary.ByteOrder) (int, error) {
-	off := 0
-	for i := range enums {
-		if off+btfEnum64Len > len(b) {
-			return 0, fmt.Errorf("not enough bytes to unmarshal btfEnum64 %d", i)
-		}
-
-		enums[i].NameOff = bo.Uint32(b[off+0:])
-		enums[i].ValLo32 = bo.Uint32(b[off+4:])
-		enums[i].ValHi32 = bo.Uint32(b[off+8:])
-
-		off += btfEnum64Len
+func unmarshalBtfEnum64(enum *btfEnum64, b []byte, bo binary.ByteOrder) (int, error) {
+	if len(b) < btfEnum64Len {
+		return 0, fmt.Errorf("not enough bytes to unmarshal btfEnum64")
 	}
 
-	return off, nil
+	enum.NameOff = bo.Uint32(b[0:])
+	enum.ValLo32 = bo.Uint32(b[4:])
+	enum.ValHi32 = bo.Uint32(b[8:])
+
+	return btfEnum64Len, nil
 }
 
 type btfParam struct {
@@ -472,20 +486,15 @@ type btfParam struct {
 
 var btfParamLen = int(unsafe.Sizeof(btfParam{}))
 
-func unmarshalBtfParams(params []btfParam, b []byte, bo binary.ByteOrder) (int, error) {
-	off := 0
-	for i := range params {
-		if off+btfParamLen > len(b) {
-			return 0, fmt.Errorf("not enough bytes to unmarshal btfParam %d", i)
-		}
-
-		params[i].NameOff = bo.Uint32(b[off+0:])
-		params[i].Type = TypeID(bo.Uint32(b[off+4:]))
-
-		off += btfParamLen
+func unmarshalBtfParam(param *btfParam, b []byte, bo binary.ByteOrder) (int, error) {
+	if len(b) < btfParamLen {
+		return 0, fmt.Errorf("not enough bytes to unmarshal btfParam")
 	}
 
-	return off, nil
+	param.NameOff = bo.Uint32(b[0:])
+	param.Type = TypeID(bo.Uint32(b[4:]))
+
+	return btfParamLen, nil
 }
 
 type btfDeclTag struct {
diff --git a/btf/core.go b/btf/core.go
index 652516a..ca4997f 100644
--- a/btf/core.go
+++ b/btf/core.go
@@ -211,8 +211,8 @@ func CORERelocate(relos []*CORERelocation, targets []*Spec, bo binary.ByteOrder,
 	resolveTargetTypeID := targets[0].TypeID
 
 	for _, target := range targets {
-		if bo != target.imm.byteOrder {
-			return nil, fmt.Errorf("can't relocate %s against %s", bo, target.imm.byteOrder)
+		if bo != target.byteOrder {
+			return nil, fmt.Errorf("can't relocate %s against %s", bo, target.byteOrder)
 		}
 	}
 
@@ -265,7 +265,7 @@ func CORERelocate(relos []*CORERelocation, targets []*Spec, bo binary.ByteOrder,
 
 		var targetTypes []Type
 		for _, target := range targets {
-			namedTypeIDs := target.imm.namedTypes[essentialName]
+			namedTypeIDs := target.TypeIDsByName(essentialName)
 			targetTypes = slices.Grow(targetTypes, len(namedTypeIDs))
 			for _, id := range namedTypeIDs {
 				typ, err := target.TypeByID(id)
diff --git a/btf/types.go b/btf/types.go
index 638e7cf..fc0a597 100644
--- a/btf/types.go
+++ b/btf/types.go
@@ -1,12 +1,10 @@
 package btf
 
 import (
-	"encoding/binary"
 	"errors"
 	"fmt"
 	"io"
 	"math"
-	"slices"
 	"strings"
 
 	"github.com/cilium/ebpf/asm"
@@ -749,510 +747,6 @@ func copyType(typ Type, ids map[Type]TypeID, copies map[Type]Type, copiedIDs map
 
 type typeDeque = internal.Deque[*Type]
 
-// readAndInflateTypes reads the raw btf type info and turns it into a graph
-// of Types connected via pointers.
-//
-// If base is provided, then the types are considered to be of a split BTF
-// (e.g., a kernel module).
-//
-// Returns a slice of types indexed by TypeID. Since BTF ignores compilation
-// units, multiple types may share the same name. A Type may form a cyclic graph
-// by pointing at itself.
-func readAndInflateTypes(r io.Reader, bo binary.ByteOrder, typeLen uint32, rawStrings *stringTable, base *Spec) ([]Type, error) {
-	// because of the interleaving between types and struct members it is difficult to
-	// precompute the numbers of raw types this will parse
-	// this "guess" is a good first estimation
-	sizeOfbtfType := uintptr(btfTypeLen)
-	tyMaxCount := uintptr(typeLen) / sizeOfbtfType / 2
-	types := make([]Type, 0, tyMaxCount)
-
-	// Void is defined to always be type ID 0, and is thus omitted from BTF.
-	types = append(types, (*Void)(nil))
-
-	firstTypeID := TypeID(0)
-	if base != nil {
-		var err error
-		firstTypeID, err = base.nextTypeID()
-		if err != nil {
-			return nil, err
-		}
-
-		// Split BTF doesn't contain Void.
-		types = types[:0]
-	}
-
-	type fixupDef struct {
-		id  TypeID
-		typ *Type
-	}
-
-	var fixups []fixupDef
-	fixup := func(id TypeID, typ *Type) {
-		if id < firstTypeID {
-			if baseType, err := base.TypeByID(id); err == nil {
-				*typ = baseType
-				return
-			}
-		}
-
-		idx := int(id - firstTypeID)
-		if idx < len(types) {
-			// We've already inflated this type, fix it up immediately.
-			*typ = types[idx]
-			return
-		}
-
-		fixups = append(fixups, fixupDef{id, typ})
-	}
-
-	type bitfieldFixupDef struct {
-		id TypeID
-		m  *Member
-	}
-
-	var (
-		legacyBitfields = make(map[TypeID][2]Bits) // offset, size
-		bitfieldFixups  []bitfieldFixupDef
-	)
-	convertMembers := func(raw []btfMember, kindFlag bool) ([]Member, error) {
-		// NB: The fixup below relies on pre-allocating this array to
-		// work, since otherwise append might re-allocate members.
-		members := make([]Member, 0, len(raw))
-		for i, btfMember := range raw {
-			name, err := rawStrings.Lookup(btfMember.NameOff)
-			if err != nil {
-				return nil, fmt.Errorf("can't get name for member %d: %w", i, err)
-			}
-
-			members = append(members, Member{
-				Name:   name,
-				Offset: Bits(btfMember.Offset),
-			})
-
-			m := &members[i]
-			fixup(raw[i].Type, &m.Type)
-
-			if kindFlag {
-				m.BitfieldSize = Bits(btfMember.Offset >> 24)
-				m.Offset &= 0xffffff
-				// We ignore legacy bitfield definitions if the current composite
-				// is a new-style bitfield. This is kind of safe since offset and
-				// size on the type of the member must be zero if kindFlat is set
-				// according to spec.
-				continue
-			}
-
-			// This may be a legacy bitfield, try to fix it up.
-			data, ok := legacyBitfields[raw[i].Type]
-			if ok {
-				// Bingo!
-				m.Offset += data[0]
-				m.BitfieldSize = data[1]
-				continue
-			}
-
-			if m.Type != nil {
-				// We couldn't find a legacy bitfield, but we know that the member's
-				// type has already been inflated. Hence we know that it can't be
-				// a legacy bitfield and there is nothing left to do.
-				continue
-			}
-
-			// We don't have fixup data, and the type we're pointing
-			// at hasn't been inflated yet. No choice but to defer
-			// the fixup.
-			bitfieldFixups = append(bitfieldFixups, bitfieldFixupDef{
-				raw[i].Type,
-				m,
-			})
-		}
-		return members, nil
-	}
-
-	var (
-		buf       = make([]byte, 1024)
-		header    btfType
-		bInt      btfInt
-		bArr      btfArray
-		bMembers  []btfMember
-		bEnums    []btfEnum
-		bParams   []btfParam
-		bVariable btfVariable
-		bSecInfos []btfVarSecinfo
-		bDeclTag  btfDeclTag
-		bEnums64  []btfEnum64
-	)
-
-	var declTags []*declTag
-	for {
-		var (
-			id  = firstTypeID + TypeID(len(types))
-			typ Type
-		)
-
-		if _, err := io.ReadFull(r, buf[:btfTypeLen]); err == io.EOF {
-			break
-		} else if err != nil {
-			return nil, fmt.Errorf("can't read type info for id %v: %v", id, err)
-		}
-
-		if _, err := unmarshalBtfType(&header, buf[:btfTypeLen], bo); err != nil {
-			return nil, fmt.Errorf("can't unmarshal type info for id %v: %v", id, err)
-		}
-
-		if id < firstTypeID {
-			return nil, fmt.Errorf("no more type IDs")
-		}
-
-		name, err := rawStrings.Lookup(header.NameOff)
-		if err != nil {
-			return nil, fmt.Errorf("get name for type id %d: %w", id, err)
-		}
-
-		switch header.Kind() {
-		case kindInt:
-			size := header.Size()
-			buf = buf[:btfIntLen]
-			if _, err := io.ReadFull(r, buf); err != nil {
-				return nil, fmt.Errorf("can't read btfInt, id: %d: %w", id, err)
-			}
-			if _, err := unmarshalBtfInt(&bInt, buf, bo); err != nil {
-				return nil, fmt.Errorf("can't unmarshal btfInt, id: %d: %w", id, err)
-			}
-			if bInt.Offset() > 0 || bInt.Bits().Bytes() != size {
-				legacyBitfields[id] = [2]Bits{bInt.Offset(), bInt.Bits()}
-			}
-			typ = &Int{name, header.Size(), bInt.Encoding()}
-
-		case kindPointer:
-			ptr := &Pointer{nil}
-			fixup(header.Type(), &ptr.Target)
-			typ = ptr
-
-		case kindArray:
-			buf = buf[:btfArrayLen]
-			if _, err := io.ReadFull(r, buf); err != nil {
-				return nil, fmt.Errorf("can't read btfArray, id: %d: %w", id, err)
-			}
-			if _, err := unmarshalBtfArray(&bArr, buf, bo); err != nil {
-				return nil, fmt.Errorf("can't unmarshal btfArray, id: %d: %w", id, err)
-			}
-
-			arr := &Array{nil, nil, bArr.Nelems}
-			fixup(bArr.IndexType, &arr.Index)
-			fixup(bArr.Type, &arr.Type)
-			typ = arr
-
-		case kindStruct:
-			vlen := header.Vlen()
-			bMembers = slices.Grow(bMembers[:0], vlen)[:vlen]
-			buf = slices.Grow(buf[:0], vlen*btfMemberLen)[:vlen*btfMemberLen]
-			if _, err := io.ReadFull(r, buf); err != nil {
-				return nil, fmt.Errorf("can't read btfMembers, id: %d: %w", id, err)
-			}
-			if _, err := unmarshalBtfMembers(bMembers, buf, bo); err != nil {
-				return nil, fmt.Errorf("can't unmarshal btfMembers, id: %d: %w", id, err)
-			}
-
-			members, err := convertMembers(bMembers, header.Bitfield())
-			if err != nil {
-				return nil, fmt.Errorf("struct %s (id %d): %w", name, id, err)
-			}
-			typ = &Struct{name, header.Size(), members, nil}
-
-		case kindUnion:
-			vlen := header.Vlen()
-			bMembers = slices.Grow(bMembers[:0], vlen)[:vlen]
-			buf = slices.Grow(buf[:0], vlen*btfMemberLen)[:vlen*btfMemberLen]
-			if _, err := io.ReadFull(r, buf); err != nil {
-				return nil, fmt.Errorf("can't read btfMembers, id: %d: %w", id, err)
-			}
-			if _, err := unmarshalBtfMembers(bMembers, buf, bo); err != nil {
-				return nil, fmt.Errorf("can't unmarshal btfMembers, id: %d: %w", id, err)
-			}
-
-			members, err := convertMembers(bMembers, header.Bitfield())
-			if err != nil {
-				return nil, fmt.Errorf("union %s (id %d): %w", name, id, err)
-			}
-			typ = &Union{name, header.Size(), members, nil}
-
-		case kindEnum:
-			vlen := header.Vlen()
-			bEnums = slices.Grow(bEnums[:0], vlen)[:vlen]
-			buf = slices.Grow(buf[:0], vlen*btfEnumLen)[:vlen*btfEnumLen]
-			if _, err := io.ReadFull(r, buf); err != nil {
-				return nil, fmt.Errorf("can't read btfEnums, id: %d: %w", id, err)
-			}
-			if _, err := unmarshalBtfEnums(bEnums, buf, bo); err != nil {
-				return nil, fmt.Errorf("can't unmarshal btfEnums, id: %d: %w", id, err)
-			}
-
-			vals := make([]EnumValue, 0, vlen)
-			signed := header.Signed()
-			for i, btfVal := range bEnums {
-				name, err := rawStrings.Lookup(btfVal.NameOff)
-				if err != nil {
-					return nil, fmt.Errorf("get name for enum value %d: %s", i, err)
-				}
-				value := uint64(btfVal.Val)
-				if signed {
-					// Sign extend values to 64 bit.
-					value = uint64(int32(btfVal.Val))
-				}
-				vals = append(vals, EnumValue{name, value})
-			}
-			typ = &Enum{name, header.Size(), signed, vals}
-
-		case kindForward:
-			typ = &Fwd{name, header.FwdKind()}
-
-		case kindTypedef:
-			typedef := &Typedef{name, nil, nil}
-			fixup(header.Type(), &typedef.Type)
-			typ = typedef
-
-		case kindVolatile:
-			volatile := &Volatile{nil}
-			fixup(header.Type(), &volatile.Type)
-			typ = volatile
-
-		case kindConst:
-			cnst := &Const{nil}
-			fixup(header.Type(), &cnst.Type)
-			typ = cnst
-
-		case kindRestrict:
-			restrict := &Restrict{nil}
-			fixup(header.Type(), &restrict.Type)
-			typ = restrict
-
-		case kindFunc:
-			fn := &Func{name, nil, header.Linkage(), nil, nil}
-			fixup(header.Type(), &fn.Type)
-			typ = fn
-
-		case kindFuncProto:
-			vlen := header.Vlen()
-			bParams = slices.Grow(bParams[:0], vlen)[:vlen]
-			buf = slices.Grow(buf[:0], vlen*btfParamLen)[:vlen*btfParamLen]
-			if _, err := io.ReadFull(r, buf); err != nil {
-				return nil, fmt.Errorf("can't read btfParams, id: %d: %w", id, err)
-			}
-			if _, err := unmarshalBtfParams(bParams, buf, bo); err != nil {
-				return nil, fmt.Errorf("can't unmarshal btfParams, id: %d: %w", id, err)
-			}
-
-			params := make([]FuncParam, 0, vlen)
-			for i, param := range bParams {
-				name, err := rawStrings.Lookup(param.NameOff)
-				if err != nil {
-					return nil, fmt.Errorf("get name for func proto parameter %d: %s", i, err)
-				}
-				params = append(params, FuncParam{
-					Name: name,
-				})
-			}
-			for i := range params {
-				fixup(bParams[i].Type, &params[i].Type)
-			}
-
-			fp := &FuncProto{nil, params}
-			fixup(header.Type(), &fp.Return)
-			typ = fp
-
-		case kindVar:
-			buf = buf[:btfVariableLen]
-			if _, err := io.ReadFull(r, buf); err != nil {
-				return nil, fmt.Errorf("can't read btfVariable, id: %d: %w", id, err)
-			}
-			if _, err := unmarshalBtfVariable(&bVariable, buf, bo); err != nil {
-				return nil, fmt.Errorf("can't read btfVariable, id: %d: %w", id, err)
-			}
-
-			v := &Var{name, nil, VarLinkage(bVariable.Linkage), nil}
-			fixup(header.Type(), &v.Type)
-			typ = v
-
-		case kindDatasec:
-			vlen := header.Vlen()
-			bSecInfos = slices.Grow(bSecInfos[:0], vlen)[:vlen]
-			buf = slices.Grow(buf[:0], vlen*btfVarSecinfoLen)[:vlen*btfVarSecinfoLen]
-			if _, err := io.ReadFull(r, buf); err != nil {
-				return nil, fmt.Errorf("can't read btfVarSecInfos, id: %d: %w", id, err)
-			}
-			if _, err := unmarshalBtfVarSecInfos(bSecInfos, buf, bo); err != nil {
-				return nil, fmt.Errorf("can't unmarshal btfVarSecInfos, id: %d: %w", id, err)
-			}
-
-			vars := make([]VarSecinfo, 0, vlen)
-			for _, btfVar := range bSecInfos {
-				vars = append(vars, VarSecinfo{
-					Offset: btfVar.Offset,
-					Size:   btfVar.Size,
-				})
-			}
-			for i := range vars {
-				fixup(bSecInfos[i].Type, &vars[i].Type)
-			}
-			typ = &Datasec{name, header.Size(), vars}
-
-		case kindFloat:
-			typ = &Float{name, header.Size()}
-
-		case kindDeclTag:
-			buf = buf[:btfDeclTagLen]
-			if _, err := io.ReadFull(r, buf); err != nil {
-				return nil, fmt.Errorf("can't read btfDeclTag, id: %d: %w", id, err)
-			}
-			if _, err := unmarshalBtfDeclTag(&bDeclTag, buf, bo); err != nil {
-				return nil, fmt.Errorf("can't read btfDeclTag, id: %d: %w", id, err)
-			}
-
-			btfIndex := bDeclTag.ComponentIdx
-			if uint64(btfIndex) > math.MaxInt {
-				return nil, fmt.Errorf("type id %d: index exceeds int", id)
-			}
-
-			dt := &declTag{nil, name, int(int32(btfIndex))}
-			fixup(header.Type(), &dt.Type)
-			typ = dt
-
-			declTags = append(declTags, dt)
-
-		case kindTypeTag:
-			tt := &TypeTag{nil, name}
-			fixup(header.Type(), &tt.Type)
-			typ = tt
-
-		case kindEnum64:
-			vlen := header.Vlen()
-			bEnums64 = slices.Grow(bEnums64[:0], vlen)[:vlen]
-			buf = slices.Grow(buf[:0], vlen*btfEnum64Len)[:vlen*btfEnum64Len]
-			if _, err := io.ReadFull(r, buf); err != nil {
-				return nil, fmt.Errorf("can't read btfEnum64s, id: %d: %w", id, err)
-			}
-			if _, err := unmarshalBtfEnums64(bEnums64, buf, bo); err != nil {
-				return nil, fmt.Errorf("can't unmarshal btfEnum64s, id: %d: %w", id, err)
-			}
-
-			vals := make([]EnumValue, 0, vlen)
-			for i, btfVal := range bEnums64 {
-				name, err := rawStrings.Lookup(btfVal.NameOff)
-				if err != nil {
-					return nil, fmt.Errorf("get name for enum64 value %d: %s", i, err)
-				}
-				value := (uint64(btfVal.ValHi32) << 32) | uint64(btfVal.ValLo32)
-				vals = append(vals, EnumValue{name, value})
-			}
-			typ = &Enum{name, header.Size(), header.Signed(), vals}
-
-		default:
-			return nil, fmt.Errorf("type id %d: unknown kind: %v", id, header.Kind())
-		}
-
-		types = append(types, typ)
-	}
-
-	for _, fixup := range fixups {
-		if fixup.id < firstTypeID {
-			return nil, fmt.Errorf("fixup for base type id %d is not expected", fixup.id)
-		}
-
-		idx := int(fixup.id - firstTypeID)
-		if idx >= len(types) {
-			return nil, fmt.Errorf("reference to invalid type id: %d", fixup.id)
-		}
-
-		*fixup.typ = types[idx]
-	}
-
-	for _, bitfieldFixup := range bitfieldFixups {
-		if bitfieldFixup.id < firstTypeID {
-			return nil, fmt.Errorf("bitfield fixup from split to base types is not expected")
-		}
-
-		data, ok := legacyBitfields[bitfieldFixup.id]
-		if ok {
-			// This is indeed a legacy bitfield, fix it up.
-			bitfieldFixup.m.Offset += data[0]
-			bitfieldFixup.m.BitfieldSize = data[1]
-		}
-	}
-
-	for _, dt := range declTags {
-		switch t := dt.Type.(type) {
-		case *Var:
-			if dt.Index != -1 {
-				return nil, fmt.Errorf("type %s: component idx %d is not -1", dt, dt.Index)
-			}
-			t.Tags = append(t.Tags, dt.Value)
-
-		case *Typedef:
-			if dt.Index != -1 {
-				return nil, fmt.Errorf("type %s: component idx %d is not -1", dt, dt.Index)
-			}
-			t.Tags = append(t.Tags, dt.Value)
-
-		case composite:
-			if dt.Index >= 0 {
-				members := t.members()
-				if dt.Index >= len(members) {
-					return nil, fmt.Errorf("type %s: component idx %d exceeds members of %s", dt, dt.Index, t)
-				}
-
-				members[dt.Index].Tags = append(members[dt.Index].Tags, dt.Value)
-				continue
-			}
-
-			if dt.Index == -1 {
-				switch t2 := t.(type) {
-				case *Struct:
-					t2.Tags = append(t2.Tags, dt.Value)
-				case *Union:
-					t2.Tags = append(t2.Tags, dt.Value)
-				}
-
-				continue
-			}
-
-			return nil, fmt.Errorf("type %s: decl tag for type %s has invalid component idx", dt, t)
-
-		case *Func:
-			fp, ok := t.Type.(*FuncProto)
-			if !ok {
-				return nil, fmt.Errorf("type %s: %s is not a FuncProto", dt, t.Type)
-			}
-
-			// Ensure the number of argument tag lists equals the number of arguments
-			if len(t.ParamTags) == 0 {
-				t.ParamTags = make([][]string, len(fp.Params))
-			}
-
-			if dt.Index >= 0 {
-				if dt.Index >= len(fp.Params) {
-					return nil, fmt.Errorf("type %s: component idx %d exceeds params of %s", dt, dt.Index, t)
-				}
-
-				t.ParamTags[dt.Index] = append(t.ParamTags[dt.Index], dt.Value)
-				continue
-			}
-
-			if dt.Index == -1 {
-				t.Tags = append(t.Tags, dt.Value)
-				continue
-			}
-
-			return nil, fmt.Errorf("type %s: decl tag for type %s has invalid component idx", dt, t)
-
-		default:
-			return nil, fmt.Errorf("type %s: decl tag for type %s is not supported", dt, t)
-		}
-	}
-
-	return types, nil
-}
-
 // essentialName represents the name of a BTF type stripped of any flavor
 // suffixes after a ___ delimiter.
 type essentialName string
diff --git a/btf/unmarshal.go b/btf/unmarshal.go
new file mode 100644
index 0000000..98cd017
--- /dev/null
+++ b/btf/unmarshal.go
@@ -0,0 +1,651 @@
+package btf
+
+import (
+	"encoding/binary"
+	"fmt"
+	"io"
+	"iter"
+	"maps"
+	"math"
+	"sync"
+)
+
+type decoder struct {
+	// Immutable fields, may be shared.
+
+	base      *decoder
+	byteOrder binary.ByteOrder
+	raw       []byte
+	strings   *stringTable
+	// The ID for offsets[0].
+	firstTypeID TypeID
+	// Map from TypeID to offset of the marshaled data in raw. Contains an entry
+	// for each TypeID, including 0 aka Void. The offset for Void is invalid.
+	offsets    []int
+	declTags   map[TypeID][]TypeID
+	namedTypes map[essentialName][]TypeID
+
+	// Protection for mutable fields below.
+	mu              sync.Mutex
+	types           map[TypeID]Type
+	typeIDs         map[Type]TypeID
+	legacyBitfields map[TypeID][2]Bits // offset, size
+}
+
+func newDecoder(raw []byte, bo binary.ByteOrder, strings *stringTable, base *decoder) (*decoder, error) {
+	firstTypeID := TypeID(0)
+	if base != nil {
+		if base.byteOrder != bo {
+			return nil, fmt.Errorf("can't use %v base with %v split BTF", base.byteOrder, bo)
+		}
+
+		if base.firstTypeID != 0 {
+			return nil, fmt.Errorf("can't use split BTF as base")
+		}
+
+		base = base.Copy()
+		firstTypeID = TypeID(len(base.offsets))
+	}
+
+	var header btfType
+	var numTypes, numDeclTags, numNamedTypes int
+
+	for _, err := range allBtfTypeOffsets(raw, bo, &header) {
+		if err != nil {
+			return nil, err
+		}
+
+		numTypes++
+
+		if header.Kind() == kindDeclTag {
+			numDeclTags++
+		}
+
+		if header.NameOff != 0 {
+			numNamedTypes++
+		}
+	}
+
+	if firstTypeID == 0 {
+		// Allocate an extra slot for Void so we don't have to deal with
+		// constant off by one issues.
+		numTypes++
+	}
+
+	offsets := make([]int, 0, numTypes)
+	declTags := make(map[TypeID][]TypeID, numDeclTags)
+	namedTypes := make(map[essentialName][]TypeID, numNamedTypes)
+
+	if firstTypeID == 0 {
+		// Add a sentinel for Void.
+		offsets = append(offsets, math.MaxInt)
+	}
+
+	id := firstTypeID + TypeID(len(offsets))
+	for offset := range allBtfTypeOffsets(raw, bo, &header) {
+		if id < firstTypeID {
+			return nil, fmt.Errorf("no more type IDs")
+		}
+
+		offsets = append(offsets, offset)
+
+		if header.Kind() == kindDeclTag {
+			declTags[header.Type()] = append(declTags[header.Type()], id)
+		}
+
+		// Build named type index.
+		name, err := strings.Lookup(header.NameOff)
+		if err != nil {
+			return nil, fmt.Errorf("lookup type name for id %v: %w", id, err)
+		}
+
+		if name := newEssentialName(name); name != "" {
+			ids := namedTypes[name]
+			if ids == nil {
+				// Almost all names will only have a single name to them.
+				// Explicitly allocate a slice of capacity 1 instead of relying
+				// on append behaviour.
+				ids = []TypeID{id}
+			} else {
+				ids = append(ids, id)
+			}
+			namedTypes[name] = ids
+		}
+
+		id++
+	}
+
+	return &decoder{
+		base,
+		bo,
+		raw,
+		strings,
+		firstTypeID,
+		offsets,
+		declTags,
+		namedTypes,
+		sync.Mutex{},
+		make(map[TypeID]Type),
+		make(map[Type]TypeID),
+		make(map[TypeID][2]Bits),
+	}, nil
+}
+
+func allBtfTypeOffsets(buf []byte, bo binary.ByteOrder, header *btfType) iter.Seq2[int, error] {
+	return func(yield func(int, error) bool) {
+		for offset := 0; offset < len(buf); {
+			start := offset
+
+			n, err := unmarshalBtfType(header, buf[offset:], bo)
+			if err != nil {
+				yield(-1, fmt.Errorf("unmarshal type header: %w", err))
+				return
+			}
+			offset += n
+
+			n, err = header.DataLen()
+			if err != nil {
+				yield(-1, err)
+				return
+			}
+			offset += n
+
+			if offset > len(buf) {
+				yield(-1, fmt.Errorf("auxiliary type data: %w", io.ErrUnexpectedEOF))
+				return
+			}
+
+			if !yield(start, nil) {
+				return
+			}
+		}
+	}
+}
+
+func (d *decoder) Copy() *decoder {
+	if d == nil {
+		return nil
+	}
+
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	types := make(map[TypeID]Type, len(d.types))
+	copiedTypes := make(map[Type]Type, len(d.types))
+	typeIDs := make(map[Type]TypeID, len(d.typeIDs))
+	for id, typ := range d.types {
+		types[id] = copyType(typ, d.typeIDs, copiedTypes, typeIDs)
+	}
+
+	return &decoder{
+		d.base,
+		d.byteOrder,
+		d.raw,
+		d.strings,
+		d.firstTypeID,
+		d.offsets,
+		d.declTags,
+		d.namedTypes,
+		sync.Mutex{},
+		types,
+		typeIDs,
+		maps.Clone(d.legacyBitfields),
+	}
+}
+
+// TypeID returns the ID for a Type previously obtained via [TypeByID].
+func (d *decoder) TypeID(typ Type) (TypeID, error) {
+	if _, ok := typ.(*Void); ok {
+		// Equality is weird for void, since it is a zero sized type.
+		return 0, nil
+	}
+
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	id, ok := d.typeIDs[typ]
+	if !ok {
+		return 0, fmt.Errorf("no ID for type %s: %w", typ, ErrNotFound)
+	}
+
+	return id, nil
+}
+
+// TypeIDsByName returns all type IDs which have the given essential name.
+//
+// The returned slice must not be modified.
+func (d *decoder) TypeIDsByName(name essentialName) []TypeID {
+	return d.namedTypes[name]
+}
+
+// TypeByID decodes a type and any of its descendants.
+func (d *decoder) TypeByID(id TypeID) (Type, error) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	return d.inflateType(id)
+}
+
+func (d *decoder) inflateType(id TypeID) (typ Type, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = r.(error)
+		}
+
+		// err is the return value of the enclosing function, even if an explicit
+		// return is used.
+		// See https://go.dev/ref/spec#Defer_statements
+		if err != nil {
+			// Remove partially inflated type so that d.types only contains
+			// fully inflated ones.
+			delete(d.types, id)
+		} else {
+			// Populate reverse index.
+			d.typeIDs[typ] = id
+		}
+	}()
+
+	if id < d.firstTypeID {
+		return d.base.inflateType(id)
+	}
+
+	if id == 0 {
+		// Void is defined to always be type ID 0, and is thus omitted from BTF.
+		// Fast-path because it is looked up frequently.
+		return (*Void)(nil), nil
+	}
+
+	if typ, ok := d.types[id]; ok {
+		return typ, nil
+	}
+
+	fixup := func(id TypeID, typ *Type) {
+		fixup, err := d.inflateType(id)
+		if err != nil {
+			panic(err)
+		}
+		*typ = fixup
+	}
+
+	convertMembers := func(header *btfType, buf []byte) ([]Member, error) {
+		var bm btfMember
+		members := make([]Member, 0, header.Vlen())
+		for i := range header.Vlen() {
+			n, err := unmarshalBtfMember(&bm, buf, d.byteOrder)
+			if err != nil {
+				return nil, fmt.Errorf("unmarshal member: %w", err)
+			}
+			buf = buf[n:]
+
+			name, err := d.strings.Lookup(bm.NameOff)
+			if err != nil {
+				return nil, fmt.Errorf("can't get name for member %d: %w", i, err)
+			}
+
+			members = append(members, Member{
+				Name:   name,
+				Offset: Bits(bm.Offset),
+			})
+
+			m := &members[i]
+			fixup(bm.Type, &m.Type)
+
+			if header.Bitfield() {
+				m.BitfieldSize = Bits(bm.Offset >> 24)
+				m.Offset &= 0xffffff
+				// We ignore legacy bitfield definitions if the current composite
+				// is a new-style bitfield. This is kind of safe since offset and
+				// size on the type of the member must be zero if kindFlat is set
+				// according to spec.
+				continue
+			}
+
+			// This may be a legacy bitfield, try to fix it up.
+			data, ok := d.legacyBitfields[bm.Type]
+			if ok {
+				// Bingo!
+				m.Offset += data[0]
+				m.BitfieldSize = data[1]
+				continue
+			}
+		}
+		return members, nil
+	}
+
+	idx := int(id - d.firstTypeID)
+	if idx >= len(d.offsets) {
+		return nil, fmt.Errorf("type id %v: %w", id, ErrNotFound)
+	}
+
+	offset := d.offsets[idx]
+	if offset >= len(d.raw) {
+		return nil, fmt.Errorf("offset out of bounds")
+	}
+
+	var (
+		header    btfType
+		bInt      btfInt
+		bArr      btfArray
+		bVariable btfVariable
+		bDeclTag  btfDeclTag
+		pos       = d.raw[offset:]
+	)
+
+	{
+		if n, err := unmarshalBtfType(&header, pos, d.byteOrder); err != nil {
+			return nil, fmt.Errorf("can't unmarshal type info for id %v: %v", id, err)
+		} else {
+			pos = pos[n:]
+		}
+
+		name, err := d.strings.Lookup(header.NameOff)
+		if err != nil {
+			return nil, fmt.Errorf("get name for type id %d: %w", id, err)
+		}
+
+		switch header.Kind() {
+		case kindInt:
+			size := header.Size()
+			if _, err := unmarshalBtfInt(&bInt, pos, d.byteOrder); err != nil {
+				return nil, fmt.Errorf("can't unmarshal btfInt, id: %d: %w", id, err)
+			}
+			if bInt.Offset() > 0 || bInt.Bits().Bytes() != size {
+				d.legacyBitfields[id] = [2]Bits{bInt.Offset(), bInt.Bits()}
+			}
+			typ = &Int{name, header.Size(), bInt.Encoding()}
+			d.types[id] = typ
+
+		case kindPointer:
+			ptr := &Pointer{nil}
+			d.types[id] = ptr
+
+			fixup(header.Type(), &ptr.Target)
+			typ = ptr
+
+		case kindArray:
+			if _, err := unmarshalBtfArray(&bArr, pos, d.byteOrder); err != nil {
+				return nil, fmt.Errorf("can't unmarshal btfArray, id: %d: %w", id, err)
+			}
+
+			arr := &Array{nil, nil, bArr.Nelems}
+			d.types[id] = arr
+
+			fixup(bArr.IndexType, &arr.Index)
+			fixup(bArr.Type, &arr.Type)
+			typ = arr
+
+		case kindStruct:
+			str := &Struct{name, header.Size(), nil, nil}
+			d.types[id] = str
+			typ = str
+
+			str.Members, err = convertMembers(&header, pos)
+			if err != nil {
+				return nil, fmt.Errorf("struct %s (id %d): %w", name, id, err)
+			}
+
+		case kindUnion:
+			uni := &Union{name, header.Size(), nil, nil}
+			d.types[id] = uni
+			typ = uni
+
+			uni.Members, err = convertMembers(&header, pos)
+			if err != nil {
+				return nil, fmt.Errorf("union %s (id %d): %w", name, id, err)
+			}
+
+		case kindEnum:
+			enum := &Enum{name, header.Size(), header.Signed(), nil}
+			d.types[id] = enum
+			typ = enum
+
+			var be btfEnum
+			enum.Values = make([]EnumValue, 0, header.Vlen())
+			for i := range header.Vlen() {
+				n, err := unmarshalBtfEnum(&be, pos, d.byteOrder)
+				if err != nil {
+					return nil, fmt.Errorf("unmarshal btfEnum %d, id: %d: %w", i, id, err)
+				}
+				pos = pos[n:]
+
+				name, err := d.strings.Lookup(be.NameOff)
+				if err != nil {
+					return nil, fmt.Errorf("get name for enum value %d: %s", i, err)
+				}
+
+				value := uint64(be.Val)
+				if enum.Signed {
+					// Sign extend values to 64 bit.
+					value = uint64(int32(be.Val))
+				}
+				enum.Values = append(enum.Values, EnumValue{name, value})
+			}
+
+		case kindForward:
+			typ = &Fwd{name, header.FwdKind()}
+			d.types[id] = typ
+
+		case kindTypedef:
+			typedef := &Typedef{name, nil, nil}
+			d.types[id] = typedef
+
+			fixup(header.Type(), &typedef.Type)
+			typ = typedef
+
+		case kindVolatile:
+			volatile := &Volatile{nil}
+			d.types[id] = volatile
+
+			fixup(header.Type(), &volatile.Type)
+			typ = volatile
+
+		case kindConst:
+			cnst := &Const{nil}
+			d.types[id] = cnst
+
+			fixup(header.Type(), &cnst.Type)
+			typ = cnst
+
+		case kindRestrict:
+			restrict := &Restrict{nil}
+			d.types[id] = restrict
+
+			fixup(header.Type(), &restrict.Type)
+			typ = restrict
+
+		case kindFunc:
+			fn := &Func{name, nil, header.Linkage(), nil, nil}
+			d.types[id] = fn
+
+			fixup(header.Type(), &fn.Type)
+			typ = fn
+
+		case kindFuncProto:
+			fp := &FuncProto{}
+			d.types[id] = fp
+
+			params := make([]FuncParam, 0, header.Vlen())
+			var bParam btfParam
+			for i := range header.Vlen() {
+				n, err := unmarshalBtfParam(&bParam, pos, d.byteOrder)
+				if err != nil {
+					return nil, fmt.Errorf("can't unmarshal btfParam %d, id: %d: %w", i, id, err)
+				}
+				pos = pos[n:]
+
+				name, err := d.strings.Lookup(bParam.NameOff)
+				if err != nil {
+					return nil, fmt.Errorf("get name for func proto parameter %d: %s", i, err)
+				}
+
+				param := FuncParam{Name: name}
+				fixup(bParam.Type, &param.Type)
+				params = append(params, param)
+			}
+
+			fixup(header.Type(), &fp.Return)
+			fp.Params = params
+			typ = fp
+
+		case kindVar:
+			if _, err := unmarshalBtfVariable(&bVariable, pos, d.byteOrder); err != nil {
+				return nil, fmt.Errorf("can't read btfVariable, id: %d: %w", id, err)
+			}
+
+			v := &Var{name, nil, VarLinkage(bVariable.Linkage), nil}
+			d.types[id] = v
+
+			fixup(header.Type(), &v.Type)
+			typ = v
+
+		case kindDatasec:
+			ds := &Datasec{name, header.Size(), nil}
+			d.types[id] = ds
+
+			vlen := header.Vlen()
+			vars := make([]VarSecinfo, 0, vlen)
+			var bSecInfo btfVarSecinfo
+			for i := 0; i < vlen; i++ {
+				n, err := unmarshalBtfVarSecInfo(&bSecInfo, pos, d.byteOrder)
+				if err != nil {
+					return nil, fmt.Errorf("can't unmarshal btfVarSecinfo %d, id: %d: %w", i, id, err)
+				}
+				pos = pos[n:]
+
+				vs := VarSecinfo{
+					Offset: bSecInfo.Offset,
+					Size:   bSecInfo.Size,
+				}
+				fixup(bSecInfo.Type, &vs.Type)
+				vars = append(vars, vs)
+			}
+			ds.Vars = vars
+			typ = ds
+
+		case kindFloat:
+			typ = &Float{name, header.Size()}
+			d.types[id] = typ
+
+		case kindDeclTag:
+			if _, err := unmarshalBtfDeclTag(&bDeclTag, pos, d.byteOrder); err != nil {
+				return nil, fmt.Errorf("can't read btfDeclTag, id: %d: %w", id, err)
+			}
+
+			btfIndex := bDeclTag.ComponentIdx
+			if uint64(btfIndex) > math.MaxInt {
+				return nil, fmt.Errorf("type id %d: index exceeds int", id)
+			}
+
+			dt := &declTag{nil, name, int(int32(btfIndex))}
+			d.types[id] = dt
+
+			fixup(header.Type(), &dt.Type)
+			typ = dt
+
+		case kindTypeTag:
+			tt := &TypeTag{nil, name}
+			d.types[id] = tt
+
+			fixup(header.Type(), &tt.Type)
+			typ = tt
+
+		case kindEnum64:
+			enum := &Enum{name, header.Size(), header.Signed(), nil}
+			d.types[id] = enum
+			typ = enum
+
+			enum.Values = make([]EnumValue, 0, header.Vlen())
+			var bEnum64 btfEnum64
+			for i := range header.Vlen() {
+				n, err := unmarshalBtfEnum64(&bEnum64, pos, d.byteOrder)
+				if err != nil {
+					return nil, fmt.Errorf("can't unmarshal btfEnum64 %d, id: %d: %w", i, id, err)
+				}
+				pos = pos[n:]
+
+				name, err := d.strings.Lookup(bEnum64.NameOff)
+				if err != nil {
+					return nil, fmt.Errorf("get name for enum64 value %d: %s", i, err)
+				}
+				value := (uint64(bEnum64.ValHi32) << 32) | uint64(bEnum64.ValLo32)
+				enum.Values = append(enum.Values, EnumValue{name, value})
+			}
+
+		default:
+			return nil, fmt.Errorf("type id %d: unknown kind: %v", id, header.Kind())
+		}
+	}
+
+	for _, tagID := range d.declTags[id] {
+		dtType, err := d.inflateType(tagID)
+		if err != nil {
+			return nil, err
+		}
+
+		dt, ok := dtType.(*declTag)
+		if !ok {
+			return nil, fmt.Errorf("type id %v: not a declTag", tagID)
+		}
+
+		switch t := typ.(type) {
+		case *Var:
+			if dt.Index != -1 {
+				return nil, fmt.Errorf("type %s: component idx %d is not -1", dt, dt.Index)
+			}
+			t.Tags = append(t.Tags, dt.Value)
+
+		case *Typedef:
+			if dt.Index != -1 {
+				return nil, fmt.Errorf("type %s: component idx %d is not -1", dt, dt.Index)
+			}
+			t.Tags = append(t.Tags, dt.Value)
+
+		case composite:
+			if dt.Index >= 0 {
+				members := t.members()
+				if dt.Index >= len(members) {
+					return nil, fmt.Errorf("type %s: component idx %d exceeds members of %s", dt, dt.Index, t)
+				}
+
+				members[dt.Index].Tags = append(members[dt.Index].Tags, dt.Value)
+			} else if dt.Index == -1 {
+				switch t2 := t.(type) {
+				case *Struct:
+					t2.Tags = append(t2.Tags, dt.Value)
+				case *Union:
+					t2.Tags = append(t2.Tags, dt.Value)
+				}
+			} else {
+				return nil, fmt.Errorf("type %s: decl tag for type %s has invalid component idx", dt, t)
+			}
+
+		case *Func:
+			fp, ok := t.Type.(*FuncProto)
+			if !ok {
+				return nil, fmt.Errorf("type %s: %s is not a FuncProto", dt, t.Type)
+			}
+
+			// Ensure the number of argument tag lists equals the number of arguments
+			if len(t.ParamTags) == 0 {
+				t.ParamTags = make([][]string, len(fp.Params))
+			}
+
+			if dt.Index >= 0 {
+				if dt.Index >= len(fp.Params) {
+					return nil, fmt.Errorf("type %s: component idx %d exceeds params of %s", dt, dt.Index, t)
+				}
+
+				t.ParamTags[dt.Index] = append(t.ParamTags[dt.Index], dt.Value)
+			} else if dt.Index == -1 {
+				t.Tags = append(t.Tags, dt.Value)
+			} else {
+				return nil, fmt.Errorf("type %s: decl tag for type %s has invalid component idx", dt, t)
+			}
+
+		default:
+			return nil, fmt.Errorf("type %s: decl tag for type %s is not supported", dt, t)
+		}
+	}
+
+	return typ, nil
+}
diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go
index 08fde8a..66c1175 100644
--- a/internal/cmd/gentypes/main.go
+++ b/internal/cmd/gentypes/main.go
@@ -165,9 +165,12 @@ import (
 
 	// Constants (aka unnamed enums)
 	var consts []btf.EnumValue
-	iter := spec.Iterate()
-	for iter.Next() {
-		e, ok := iter.Type.(*btf.Enum)
+	for typ, err := range spec.All() {
+		if err != nil {
+			return nil, err
+		}
+
+		e, ok := typ.(*btf.Enum)
 		if !ok {
 			continue
 		}


Test patch:
diff --git a/btf/btf_test.go b/btf/btf_test.go
index 37fa8d0..35fca41 100644
--- a/btf/btf_test.go
+++ b/btf/btf_test.go
@@ -226,10 +226,30 @@ func BenchmarkParseVmlinux(b *testing.B) {
 	}
 }
 
+func BenchmarkIterateVmlinux(b *testing.B) {
+	rd := vmlinuxTestdataReader(b)
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for range b.N {
+		if _, err := rd.Seek(0, io.SeekStart); err != nil {
+			b.Fatal(err)
+		}
+
+		spec, err := loadRawSpec(rd, binary.LittleEndian, nil)
+		if err != nil {
+			b.Fatal("Can't load BTF:", err)
+		}
+
+		for range spec.All() {
+		}
+	}
+}
+
 func TestParseCurrentKernelBTF(t *testing.T) {
 	spec := vmlinuxSpec(t)
 
-	if len(spec.imm.namedTypes) == 0 {
+	if len(spec.namedTypes) == 0 {
 		t.Fatal("Empty kernel BTF")
 	}
 
@@ -260,7 +280,7 @@ func TestFindVMLinux(t *testing.T) {
 		t.Fatal("Can't load BTF:", err)
 	}
 
-	if len(spec.imm.namedTypes) == 0 {
+	if len(spec.namedTypes) == 0 {
 		t.Fatal("Empty kernel BTF")
 	}
 }
@@ -331,10 +351,10 @@ func TestSpecCopy(t *testing.T) {
 	spec := parseELFBTF(t, "../testdata/loader-el.elf")
 	cpy := spec.Copy()
 
-	have := typesFromSpec(spec)
+	have := typesFromSpec(t, spec)
 	qt.Assert(t, qt.IsTrue(len(have) > 0))
 
-	want := typesFromSpec(cpy)
+	want := typesFromSpec(t, cpy)
 	qt.Assert(t, qt.HasLen(want, len(have)))
 
 	for i := range want {
@@ -417,19 +437,13 @@ func TestTypesIterator(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	iter := spec.Iterate()
-
-	for i, typ := range types {
-		if !iter.Next() {
-			t.Fatal("Iterator ended early at item", i)
-		}
-
-		qt.Assert(t, qt.DeepEquals(iter.Type, typ))
+	var have []Type
+	for typ, err := range spec.All() {
+		qt.Assert(t, qt.IsNil(err))
+		have = append(have, typ)
 	}
 
-	if iter.Next() {
-		t.Fatalf("Iterator yielded too many items: %p (%[1]T)", iter.Type)
-	}
+	qt.Assert(t, qt.DeepEquals(have, types))
 }
 
 func TestLoadSplitSpecFromReader(t *testing.T) {
@@ -570,3 +584,57 @@ func BenchmarkSpecTypeByID(b *testing.B) {
 		}
 	}
 }
+
+func BenchmarkInspectorGadget(b *testing.B) {
+	// This benchmark is the baseline for what Inspektor Gadget loads for a
+	// common configuration.
+	types := []string{
+		"pt_regs",
+		"file",
+		"inode",
+		"super_block",
+		"socket",
+		"syscall_trace_enter",
+		"task_struct",
+		"nsproxy",
+		"mnt_namespace",
+		// "fanotify_event",
+		"pid",
+		"trace_event_raw_sched_process_exec",
+		"fs_struct",
+		"path",
+		"mount",
+		"qstr",
+		"vfsmount",
+		"dentry",
+		// "bpf_func_id",
+		"mm_struct",
+		"syscall_trace_exit",
+		"linux_binprm",
+		"sock",
+		"net",
+		"inet_sock",
+	}
+
+	vmlinux, err := internal.ReadAllCompressed("testdata/vmlinux.btf.gz")
+	qt.Assert(b, qt.IsNil(err))
+
+	var rd bytes.Reader
+
+	b.ResetTimer()
+
+	for range b.N {
+		rd.Reset(vmlinux)
+		spec, err := LoadSpecFromReader(&rd)
+		if err != nil {
+			b.Fatal(err)
+		}
+
+		var s *Struct
+		for _, name := range types {
+			if err := spec.TypeByName(name, &s); err != nil {
+				b.Fatal(name, err)
+			}
+		}
+	}
+}
diff --git a/btf/core_test.go b/btf/core_test.go
index 3d58355..ed7bfac 100644
--- a/btf/core_test.go
+++ b/btf/core_test.go
@@ -558,7 +558,7 @@ func TestCORERelocation(t *testing.T) {
 					relos = append(relos, reloInfo.relo)
 				}
 
-				fixups, err := CORERelocate(relos, []*Spec{spec}, spec.imm.byteOrder, spec.TypeID)
+				fixups, err := CORERelocate(relos, []*Spec{spec}, spec.byteOrder, spec.TypeID)
 				if want := errs[name]; want != nil {
 					if !errors.Is(err, want) {
 						t.Fatal("Expected", want, "got", err)
@@ -696,7 +696,7 @@ func BenchmarkCORESkBuff(b *testing.B) {
 			b.ReportAllocs()
 
 			for i := 0; i < b.N; i++ {
-				_, err = CORERelocate([]*CORERelocation{relo}, []*Spec{spec}, spec.imm.byteOrder, spec.TypeID)
+				_, err = CORERelocate([]*CORERelocation{relo}, []*Spec{spec}, spec.byteOrder, spec.TypeID)
 				if err != nil {
 					b.Fatal(err)
 				}
diff --git a/btf/fuzz_test.go b/btf/fuzz_test.go
index df93cf5..e98cdce 100644
--- a/btf/fuzz_test.go
+++ b/btf/fuzz_test.go
@@ -32,9 +32,10 @@ func FuzzSpec(f *testing.F) {
 			t.Fatal("spec is nil")
 		}
 
-		iter := spec.Iterate()
-		for iter.Next() {
-			fmt.Fprintf(io.Discard, "%+10v", iter.Type)
+		for typ, err := range spec.All() {
+			if err == nil {
+				fmt.Fprintf(io.Discard, "%+10v", typ)
+			}
 		}
 	})
 }
diff --git a/btf/marshal_test.go b/btf/marshal_test.go
index f27f6df..5b45ab9 100644
--- a/btf/marshal_test.go
+++ b/btf/marshal_test.go
@@ -37,7 +37,7 @@ func TestBuilderMarshal(t *testing.T) {
 
 	have, err := loadRawSpec(bytes.NewReader(buf), internal.NativeEndian, nil)
 	qt.Assert(t, qt.IsNil(err), qt.Commentf("Couldn't parse BTF"))
-	qt.Assert(t, qt.DeepEquals(have.imm.types, want))
+	qt.Assert(t, qt.DeepEquals(typesFromSpec(t, have), want))
 }
 
 func TestBuilderAdd(t *testing.T) {
@@ -71,7 +71,7 @@ func TestBuilderAdd(t *testing.T) {
 }
 
 func TestRoundtripVMlinux(t *testing.T) {
-	types := typesFromSpec(vmlinuxSpec(t))
+	types := typesFromSpec(t, vmlinuxSpec(t))
 
 	// Randomize the order to force different permutations of walking the type
 	// graph. Keep Void at index 0.
@@ -101,7 +101,7 @@ limitTypes:
 	rebuilt, err := loadRawSpec(bytes.NewReader(buf), binary.LittleEndian, nil)
 	qt.Assert(t, qt.IsNil(err), qt.Commentf("round tripping BTF failed"))
 
-	if n := len(rebuilt.imm.types); n > math.MaxUint16 {
+	if n := len(rebuilt.offsets); n > math.MaxUint16 {
 		t.Logf("Rebuilt BTF contains %d types which exceeds uint16, test may fail on older kernels", n)
 	}
 
@@ -209,7 +209,7 @@ func TestMarshalTypeTags(t *testing.T) {
 }
 
 func BenchmarkMarshaler(b *testing.B) {
-	types := typesFromSpec(vmlinuxTestdataSpec(b))[:100]
+	types := typesFromSpec(b, vmlinuxTestdataSpec(b))[:100]
 
 	b.ReportAllocs()
 	b.ResetTimer()
@@ -224,7 +224,7 @@ func BenchmarkMarshaler(b *testing.B) {
 }
 
 func BenchmarkBuildVmlinux(b *testing.B) {
-	types := typesFromSpec(vmlinuxTestdataSpec(b))
+	types := typesFromSpec(b, vmlinuxTestdataSpec(b))
 
 	b.ReportAllocs()
 	b.ResetTimer()
@@ -258,11 +258,14 @@ func specFromTypes(tb testing.TB, types []Type) *Spec {
 	return spec
 }
 
-func typesFromSpec(spec *Spec) []Type {
-	var types []Type
-	iter := spec.Iterate()
-	for iter.Next() {
-		types = append(types, iter.Type)
+func typesFromSpec(tb testing.TB, spec *Spec) []Type {
+	tb.Helper()
+
+	types := make([]Type, 0, len(spec.offsets))
+
+	for typ, err := range spec.All() {
+		qt.Assert(tb, qt.IsNil(err))
+		types = append(types, typ)
 	}
 
 	return types
diff --git a/btf/strings_test.go b/btf/strings_test.go
index 7c1510d..7439cc0 100644
--- a/btf/strings_test.go
+++ b/btf/strings_test.go
@@ -102,15 +102,3 @@ func BenchmarkStringTableZeroLookup(b *testing.B) {
 		}
 	}
 }
-
-func newStringTable(strings ...string) *stringTable {
-	offsets := make([]uint32, len(strings))
-
-	var offset uint32
-	for i, str := range strings {
-		offsets[i] = offset
-		offset += uint32(len(str)) + 1 // account for NUL
-	}
-
-	return &stringTable{nil, offsets, 0, strings}
-}
diff --git a/btf/types_test.go b/btf/types_test.go
index 54695f4..3d6ef2d 100644
--- a/btf/types_test.go
+++ b/btf/types_test.go
@@ -387,6 +387,31 @@ func TestInflateLegacyBitfield(t *testing.T) {
 	const offset = 3
 	const size = 5
 
+	addHeaderAndStringTable := func(types ...any) []byte {
+		var buf []byte
+		var err error
+		for _, typ := range types {
+			buf, err = binary.Append(buf, binary.LittleEndian, typ)
+			qt.Assert(t, qt.IsNil(err))
+		}
+
+		header, err := binary.Append(nil, binary.LittleEndian, &btfHeader{
+			Magic:     btfMagic,
+			Version:   1,
+			Flags:     0,
+			HdrLen:    uint32(btfHeaderLen),
+			TypeOff:   0,
+			TypeLen:   uint32(len(buf)),
+			StringOff: uint32(len(buf)),
+			StringLen: 1,
+		})
+		qt.Assert(t, qt.IsNil(err))
+
+		buf = append(header, buf...)
+		buf = append(buf, 0) // string table
+		return buf
+	}
+
 	var placeholder struct {
 		btfType
 		btfInt
@@ -404,20 +429,12 @@ func TestInflateLegacyBitfield(t *testing.T) {
 	structFirst.SetVlen(1)
 	structFirst.Members = [...]btfMember{{Type: 2}}
 
-	before, err := binary.Append(nil, binary.LittleEndian, &structFirst)
-	qt.Assert(t, qt.IsNil(err))
-	before, err = binary.Append(before, binary.LittleEndian, &placeholder)
-	qt.Assert(t, qt.IsNil(err))
+	before := addHeaderAndStringTable(&structFirst, &placeholder)
 
 	structSecond := structFirst
 	structSecond.Members = [...]btfMember{{Type: 1}}
 
-	after, err := binary.Append(nil, binary.LittleEndian, &placeholder)
-	qt.Assert(t, qt.IsNil(err))
-	after, err = binary.Append(after, binary.LittleEndian, &structSecond)
-	qt.Assert(t, qt.IsNil(err))
-
-	emptyStrings := newStringTable("")
+	after := addHeaderAndStringTable(&placeholder, &structSecond)
 
 	for _, test := range []struct {
 		name string
@@ -427,13 +444,10 @@ func TestInflateLegacyBitfield(t *testing.T) {
 		{"struct after int", after},
 	} {
 		t.Run(test.name, func(t *testing.T) {
-			types, err := readAndInflateTypes(bytes.NewReader(test.buf), binary.LittleEndian, 2, emptyStrings, nil)
-			if err != nil {
-				t.Log(test.buf)
-				t.Fatal(err)
-			}
+			spec, err := loadRawSpec(bytes.NewReader(test.buf), binary.LittleEndian, nil)
+			qt.Assert(t, qt.IsNil(err))
 
-			for _, typ := range types {
+			for _, typ := range typesFromSpec(t, spec) {
 				s, ok := typ.(*Struct)
 				if !ok {
 					continue