You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fory.apache.org by ch...@apache.org on 2026/08/04 10:50:00 UTC

(fory) branch main updated: perf(swift): optimize swift serialization performance (#3921)

This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 5eb14cda6 perf(swift): optimize swift serialization performance (#3921)
5eb14cda6 is described below

commit 5eb14cda66fe1266e81f71233ea5aabf448587ad
Author: Shawn Yang <sh...@gmail.com>
AuthorDate: Tue Aug 4 18:49:54 2026 +0800

    perf(swift): optimize swift serialization performance (#3921)
    
    <!--
    **Thanks for contributing to Apache Fory™.**
    
    **If this is your first time opening a PR on fory, you can refer to
    [CONTRIBUTING.md](https://github.com/apache/fory/blob/main/CONTRIBUTING.md).**
    
    Contribution Checklist
    
    - The **Apache Fory™** community has requirements on the naming of pr
    titles. You can also find instructions in
    [CONTRIBUTING.md](https://github.com/apache/fory/blob/main/CONTRIBUTING.md).
    
    - Apache Fory™ has a strong focus on performance. If the PR you submit
    will have an impact on performance, please benchmark it first and
    provide the benchmark result here.
    -->
    
    ## Why?
    
    Swift's dynamic exclusivity checks and generic collection/String paths
    add measurable overhead to
    serialization and deserialization, especially for small structs and
    collection-heavy objects.
    
    ## What does this PR do?
    
    - Removes dynamic exclusivity checks from synchronous `ByteBuffer` state
    access.
    - Specializes packed primitive-array reads and avoids zero-initializing
    destination arrays.
    - Accelerates ASCII String detection with eight-byte checks while
    preserving UTF-8 validation.
    - Adds a direct fast path for non-null Array elements.
    - Uses the TypeMeta header hash directly for exact-schema compatible
    reads.
    - Refreshes the Swift benchmark report and throughput plot.
    
    ## Related issues
    
    Closes #3460
    
    ## AI Contribution Checklist
    
    <!-- Full requirements and disclosure template:
    
    https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs
    -->
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence or equivalent persisted links of the
    final clean AI review results from both fresh reviewers described in
    `AI_POLICY.md`, the Fory-guided reviewer and the independent general
    reviewer, on the current PR diff or current HEAD after the latest code
    changes.
    
    <!-- If substantial AI assistance = `yes`, paste the completed checklist
    and disclosure block here, including the final ai_review summary and
    screenshot evidence or equivalent persisted links from both fresh
    reviewers described in AI_POLICY.md, the Fory-guided reviewer and the
    independent general reviewer, on the current PR diff or current HEAD
    after the latest code changes. -->
    
    ## Does this PR introduce any user-facing change?
    
    <!--
    If any user-facing interface changes, please [open an
    issue](https://github.com/apache/fory/issues/new/choose) describing the
    need to do so and update the document if necessary.
    
    Delete section if not applicable.
    -->
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
    
    Environment: Apple Swift 6.2.1. The complete benchmark used three
    seconds per case:
    
    ```shell
    cd benchmarks/swift
    ./run.sh
    ```
    
    | Datatype | Operation | Fory ops/s | Protobuf ops/s | JSON ops/s |
    | --- | --- | ---: | ---: | ---: |
    | NumericStruct | Serialize | 14,759,606 | 7,572,190 | 481,278 |
    | NumericStruct | Deserialize | 18,155,795 | 7,317,942 | 377,802 |
    | Sample | Serialize | 5,633,231 | 1,380,130 | 102,372 |
    | Sample | Deserialize | 1,509,740 | 916,626 | 52,897 |
    | MediaContent | Serialize | 2,987,821 | 784,863 | 111,200 |
    | MediaContent | Deserialize | 1,072,345 | 549,726 | 97,799 |
    | NumericStructList | Serialize | 5,515,769 | 1,093,924 | 93,635 |
    | NumericStructList | Deserialize | 2,598,609 | 746,527 | 76,161 |
    | SampleList | Serialize | 1,201,868 | 220,772 | 20,772 |
    | SampleList | Deserialize | 286,949 | 161,192 | 10,777 |
    | MediaContentList | Serialize | 657,276 | 118,004 | 22,744 |
    | MediaContentList | Deserialize | 215,726 | 99,445 | 19,521 |
---
 AGENTS.md                                          |   2 +-
 benchmarks/swift/README.md                         |   4 +-
 benchmarks/swift/benchmark_report.py               |  11 +-
 .../object-serialization/xlang/swift/README.md     |  35 +--
 .../xlang/swift/throughput.png                     | Bin 107942 -> 106749 bytes
 swift/Sources/Fory/ByteBuffer.swift                |  56 ++--
 swift/Sources/Fory/CollectionSerializers.swift     | 332 ++++++++++++++-------
 swift/Sources/Fory/FieldCodecs.swift               |  10 +-
 swift/Sources/Fory/TypeMeta.swift                  |   2 +
 .../ForyMacro/ForyObjectMacroReadGeneration.swift  |  18 +-
 10 files changed, 292 insertions(+), 178 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 1d458373f..3f24db55c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -139,7 +139,7 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th
   or map entry may instead advance because of ref, null, or type envelopes; name those derived
   facts `fieldReadAlwaysAdvances`, `elementReadAlwaysAdvances`, or `entryReadAlwaysAdvances` rather
   than conflating them with `readData`.
-- For remote TypeDef/TypeMeta reads, the checked metadata cache is the only owner of remote "already validated" state. Cache hit means the header was previously parsed, body/hash-validated, policy-checked, and published by that cache, so the hot path must skip the body and use cached metadata without extra validation, hashing, limit checks, exact-local checks, allocation, or policy work. A known expected local TypeDef/TypeMeta header/hash match is a local-schema hit, not a remote cache m [...]
+- For remote TypeDef/TypeMeta reads, the checked metadata cache is the only owner of remote "already validated" state. Cache hit means the header was previously parsed, body/hash-validated, policy-checked, and published by that cache, so the hot path must skip the body and use cached metadata without extra validation, hashing, limit checks, exact-local checks, allocation, or policy work. The protocol-defined 52-bit TypeDef/TypeMeta header hash is the unique schema identity, so a known ex [...]
 - When a user corrects a non-obvious invariant, encode it in the nearest source comment before continuing, and also update `AGENTS.md`, `.agents/**`, docs, or specs when the rule is reusable beyond one file. Do not rely only on chat history, task notes, commit messages, or benchmark logs for corrections that protect security, protocol behavior, ownership, naming, or hot-path performance.
 - Reject semantic hacks. Do not bypass broken semantics by deleting cases, simplifying callers, adding coercion hooks, or using workaround fallbacks; fix the underlying bug and prove it with focused tests.
 - Protect hot paths. Avoid per-call allocations, callback objects, result tuples or records, unnecessary runtime branches, and wrapper-class substitutions in hot codec/runtime paths; prefer conditional imports and allocation-free concrete implementations where they fit the language.
diff --git a/benchmarks/swift/README.md b/benchmarks/swift/README.md
index a5b4e1101..c1b8a5b74 100644
--- a/benchmarks/swift/README.md
+++ b/benchmarks/swift/README.md
@@ -119,6 +119,6 @@ protoc \
 ```
 
 - The benchmark intentionally includes plain-model conversion for protobuf to mirror real-world usage.
-- `swift-benchmark` and `swift-external-benchmark` are separate products. Building the ordinary
-  product does not compile external-type models or serializer specializations.
+- `swift-benchmark` and `swift-external-benchmark` are separate executables. Building the ordinary
+  benchmark does not compile external-type models or serializer specializations.
 - Results vary across machines and runtime environments.
diff --git a/benchmarks/swift/benchmark_report.py b/benchmarks/swift/benchmark_report.py
index cf0e717e1..1ea4c403e 100755
--- a/benchmarks/swift/benchmark_report.py
+++ b/benchmarks/swift/benchmark_report.py
@@ -265,13 +265,12 @@ def write_report(
         "Apache Fory, Protocol Buffers, and JSON in Swift."
     )
     lines.append("")
-    lines.append("## Benchmark Products")
+    lines.append("## Benchmark Scope")
     lines.append("")
     lines.append(
-        "The ordinary/xlang cases are built by `swift-benchmark`. External-type "
-        "and carrier comparisons are built by the separate "
-        "`swift-external-benchmark` product, so building the ordinary product "
-        "does not compile those models or serializer specializations."
+        "The results below cover the ordinary/xlang cases run by `swift-benchmark`. "
+        "External-type and carrier comparisons use the separate "
+        "`swift-external-benchmark` executable and are included only when requested."
     )
     lines.append("")
     lines.append("## Throughput Plot")
@@ -344,7 +343,7 @@ def write_report(
         lines.append("## External-Type Serialization")
         lines.append("")
         lines.append(
-            "These cases are built in the isolated `swift-external-benchmark` product."
+            "These cases run in the isolated `swift-external-benchmark` executable."
         )
         lines.append("")
         lines.append("| Case | Operation | TPS | ns/op | Bytes |")
diff --git a/docs/benchmarks/object-serialization/xlang/swift/README.md b/docs/benchmarks/object-serialization/xlang/swift/README.md
index 91d456822..5da688839 100644
--- a/docs/benchmarks/object-serialization/xlang/swift/README.md
+++ b/docs/benchmarks/object-serialization/xlang/swift/README.md
@@ -2,12 +2,9 @@
 
 This benchmark compares serialization and deserialization throughput for Apache Fory, Protocol Buffers, and JSON in Swift.
 
-## Benchmark Executables
+## Benchmark Scope
 
-The ordinary/xlang cases are built by `swift-benchmark`. External-type and
-carrier comparisons are built by the separate `swift-external-benchmark`
-executable, so building the ordinary executable does not compile those models or
-serializer specializations.
+The results below cover the ordinary/xlang cases run by `swift-benchmark`. External-type and carrier comparisons use the separate `swift-external-benchmark` executable and are included only when requested.
 
 ## Throughput Plot
 
@@ -17,9 +14,9 @@ serializer specializations.
 
 | Key                   | Value                         |
 | --------------------- | ----------------------------- |
-| Timestamp             | 2026-05-08T09:05:32Z          |
+| Timestamp             | 2026-08-04T09:52:12Z          |
 | OS                    | Version 15.7.2 (Build 24G325) |
-| Host                  | macbook-pro.local             |
+| Host                  | MacBook-Pro.local             |
 | CPU Cores (Logical)   | 12                            |
 | Memory (GB)           | 48.00                         |
 | Duration per case (s) | 3                             |
@@ -28,18 +25,18 @@ serializer specializations.
 
 | Datatype          | Operation   |   Fory TPS | Protobuf TPS | JSON TPS | Fastest      |
 | ----------------- | ----------- | ---------: | -----------: | -------: | ------------ |
-| NumericStruct     | Serialize   |  9,435,623 |    6,175,939 |  408,960 | fory (1.53x) |
-| NumericStruct     | Deserialize | 11,037,225 |    6,842,676 |  328,302 | fory (1.61x) |
-| Sample            | Serialize   |  3,596,835 |    1,257,100 |   79,781 | fory (2.86x) |
-| Sample            | Deserialize |    982,255 |      733,588 |   41,274 | fory (1.34x) |
-| MediaContent      | Serialize   |  1,561,376 |      609,896 |   98,677 | fory (2.56x) |
-| MediaContent      | Deserialize |    523,836 |      395,202 |   70,528 | fory (1.33x) |
-| NumericStructList | Serialize   |  2,910,846 |      918,363 |   82,965 | fory (3.17x) |
-| NumericStructList | Deserialize |  2,436,636 |      701,656 |   69,353 | fory (3.47x) |
-| SampleList        | Serialize   |    694,557 |      202,040 |   16,679 | fory (3.44x) |
-| SampleList        | Deserialize |    187,109 |      131,947 |    8,236 | fory (1.42x) |
-| MediaContentList  | Serialize   |    348,238 |       98,007 |   18,698 | fory (3.55x) |
-| MediaContentList  | Deserialize |    104,990 |       74,422 |   16,298 | fory (1.41x) |
+| NumericStruct     | Serialize   | 14,759,606 |    7,572,190 |  481,278 | fory (1.95x) |
+| NumericStruct     | Deserialize | 18,155,795 |    7,317,942 |  377,802 | fory (2.48x) |
+| Sample            | Serialize   |  5,633,231 |    1,380,130 |  102,372 | fory (4.08x) |
+| Sample            | Deserialize |  1,509,740 |      916,626 |   52,897 | fory (1.65x) |
+| MediaContent      | Serialize   |  2,987,821 |      784,863 |  111,200 | fory (3.81x) |
+| MediaContent      | Deserialize |  1,072,345 |      549,726 |   97,799 | fory (1.95x) |
+| NumericStructList | Serialize   |  5,515,769 |    1,093,924 |   93,635 | fory (5.04x) |
+| NumericStructList | Deserialize |  2,598,609 |      746,527 |   76,161 | fory (3.48x) |
+| SampleList        | Serialize   |  1,201,868 |      220,772 |   20,772 | fory (5.44x) |
+| SampleList        | Deserialize |    286,949 |      161,192 |   10,777 | fory (1.78x) |
+| MediaContentList  | Serialize   |    657,276 |      118,004 |   22,744 | fory (5.57x) |
+| MediaContentList  | Deserialize |    215,726 |       99,445 |   19,521 | fory (2.17x) |
 
 ## Serialized Size (bytes)
 
diff --git a/docs/benchmarks/object-serialization/xlang/swift/throughput.png b/docs/benchmarks/object-serialization/xlang/swift/throughput.png
index c8d7f75e3..3e1ee7354 100644
Binary files a/docs/benchmarks/object-serialization/xlang/swift/throughput.png and b/docs/benchmarks/object-serialization/xlang/swift/throughput.png differ
diff --git a/swift/Sources/Fory/ByteBuffer.swift b/swift/Sources/Fory/ByteBuffer.swift
index c4c7fd812..582e7d748 100644
--- a/swift/Sources/Fory/ByteBuffer.swift
+++ b/swift/Sources/Fory/ByteBuffer.swift
@@ -18,12 +18,17 @@
 import Foundation
 
 public final class ByteBuffer {
+    // Buffer state access is synchronous and never re-entered while borrowed. Overlapping access is
+    // invalid because unchecked exclusivity removes Swift's runtime enforcement on these hot paths.
     @usableFromInline
+    @exclusivity(unchecked)
     internal var storage: [UInt8]
 
     @usableFromInline
+    @exclusivity(unchecked)
     internal var cursor: Int
 
+    @exclusivity(unchecked)
     private var dataBridge = Data()
 
     @inlinable
@@ -180,26 +185,6 @@ public final class ByteBuffer {
         return dataBridge
     }
 
-    @usableFromInline
-    @inline(__always)
-    internal func materializeData(
-        byteCount: Int,
-        _ body: (UnsafeMutablePointer<UInt8>) -> Void
-    ) -> Data {
-        if dataBridge.count != byteCount {
-            dataBridge.count = byteCount
-        }
-        if byteCount > 0 {
-            dataBridge.withUnsafeMutableBytes { destination in
-                guard let base = destination.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
-                    return
-                }
-                body(base)
-            }
-        }
-        return dataBridge
-    }
-
     @inlinable
     public func getCursor() -> Int {
         cursor
@@ -837,7 +822,36 @@ public final class ByteBuffer {
             guard let base = buffer.baseAddress else {
                 return nil
             }
-            let utf8Bytes = UnsafeBufferPointer(start: base.advanced(by: start), count: count)
+            let utf8Base = base.advanced(by: start)
+            let utf8Bytes = UnsafeBufferPointer(start: utf8Base, count: count)
+            var index = 0
+            var isASCII = true
+            // An ASCII byte has a clear high bit, so this mask checks eight bytes at once
+            // regardless of native byte order. The bounded loop makes the unaligned load safe.
+            while index <= count - MemoryLayout<UInt64>.size {
+                let word = UnsafeRawPointer(utf8Base.advanced(by: index))
+                    .loadUnaligned(as: UInt64.self)
+                if word & 0x8080_8080_8080_8080 != 0 {
+                    isASCII = false
+                    break
+                }
+                index += MemoryLayout<UInt64>.size
+            }
+            if isASCII {
+                while index < count {
+                    if utf8Bytes[index] >= 0x80 {
+                        isASCII = false
+                        break
+                    }
+                    index += 1
+                }
+            }
+            if isASCII {
+                return String(decoding: utf8Bytes, as: UTF8.self)
+            }
+            if #available(macOS 15.0, iOS 18.0, *) {
+                return String(validating: utf8Bytes, as: UTF8.self)
+            }
             return String(bytes: utf8Bytes, encoding: .utf8)
         }
         guard let decoded else {
diff --git a/swift/Sources/Fory/CollectionSerializers.swift b/swift/Sources/Fory/CollectionSerializers.swift
index fde1c2356..0d351cced 100644
--- a/swift/Sources/Fory/CollectionSerializers.swift
+++ b/swift/Sources/Fory/CollectionSerializers.swift
@@ -367,23 +367,211 @@ private func preparePrimitiveArray<Element: Serializer>(
     type: Element.Type,
     count: Int,
     label: String
+) throws {
+    try preparePrimitiveArrayStorage(
+        context,
+        reserveGraphStorage: reserveGraphStorage,
+        ownerBytes: storedOwnerBytes([Element].self),
+        count: count,
+        elementBytes: storedElementBytes(type),
+        label: label
+    )
+}
+
+@inline(__always)
+private func preparePrimitiveArrayStorage(
+    _ context: ReadContext,
+    reserveGraphStorage: Bool,
+    ownerBytes: Int,
+    count: Int,
+    elementBytes: Int,
+    label: String
 ) throws {
     try context.ensureCollectionLength(count, label: label)
     if reserveGraphStorage {
-        try reserveGraphArrayMemory(
-            context, type, ownerBytes: storedOwnerBytes([Element].self), count: count)
+        try reserveGraphElements(
+            context, ownerBytes: ownerBytes, count: count, elementBytes: elementBytes)
+    }
+}
+
+@inline(__always)
+private func readPrimitiveArrayByteSize(_ context: ReadContext) throws -> Int {
+    let byteSize = Int(try context.buffer.readVarUInt32())
+    try context.ensureRemainingBytes(byteSize, label: "primitive_array_bytes")
+    return byteSize
+}
+
+@inline(__always)
+func readBoolPrimitiveArray(
+    _ context: ReadContext,
+    reserveGraphStorage: Bool = false
+) throws -> [Bool] {
+    let byteSize = try readPrimitiveArrayByteSize(context)
+    try preparePrimitiveArrayStorage(
+        context,
+        reserveGraphStorage: reserveGraphStorage,
+        ownerBytes: MemoryLayout<[Bool]>.stride,
+        count: byteSize,
+        elementBytes: MemoryLayout<Bool>.stride,
+        label: "bool_array"
+    )
+    return try readArrayUninitialized(count: byteSize) { destination in
+        for index in 0..<byteSize {
+            destination.advanced(by: index).initialize(to: try context.buffer.readUInt8() != 0)
+        }
+    }
+}
+
+@inline(__always)
+func readInt32PrimitiveArray(
+    _ context: ReadContext,
+    reserveGraphStorage: Bool = false
+) throws -> [Int32] {
+    let byteSize = try readPrimitiveArrayByteSize(context)
+    if byteSize % 4 != 0 { throw primitiveArraySizeMismatch("int32") }
+    let count = byteSize / 4
+    try preparePrimitiveArrayStorage(
+        context,
+        reserveGraphStorage: reserveGraphStorage,
+        ownerBytes: MemoryLayout<[Int32]>.stride,
+        count: count,
+        elementBytes: MemoryLayout<Int32>.stride,
+        label: "int32_array"
+    )
+    if hostIsLittleEndian {
+        return try readArrayUninitialized(count: count) { destination in
+            try context.buffer.readBytes(
+                into: UnsafeMutableRawBufferPointer(start: destination, count: byteSize))
+        }
+    }
+    return try readArrayUninitialized(count: count) { destination in
+        for index in 0..<count {
+            destination.advanced(by: index).initialize(to: try context.buffer.readInt32())
+        }
+    }
+}
+
+@inline(__always)
+func readInt64PrimitiveArray(
+    _ context: ReadContext,
+    reserveGraphStorage: Bool = false
+) throws -> [Int64] {
+    let byteSize = try readPrimitiveArrayByteSize(context)
+    if byteSize % 8 != 0 { throw primitiveArraySizeMismatch("int64") }
+    let count = byteSize / 8
+    try preparePrimitiveArrayStorage(
+        context,
+        reserveGraphStorage: reserveGraphStorage,
+        ownerBytes: MemoryLayout<[Int64]>.stride,
+        count: count,
+        elementBytes: MemoryLayout<Int64>.stride,
+        label: "int64_array"
+    )
+    if hostIsLittleEndian {
+        return try readArrayUninitialized(count: count) { destination in
+            try context.buffer.readBytes(
+                into: UnsafeMutableRawBufferPointer(start: destination, count: byteSize))
+        }
+    }
+    return try readArrayUninitialized(count: count) { destination in
+        for index in 0..<count {
+            destination.advanced(by: index).initialize(to: try context.buffer.readInt64())
+        }
+    }
+}
+
+@inline(__always)
+func readFloatPrimitiveArray(
+    _ context: ReadContext,
+    reserveGraphStorage: Bool = false
+) throws -> [Float] {
+    let byteSize = try readPrimitiveArrayByteSize(context)
+    if byteSize % 4 != 0 { throw primitiveArraySizeMismatch("float32") }
+    let count = byteSize / 4
+    try preparePrimitiveArrayStorage(
+        context,
+        reserveGraphStorage: reserveGraphStorage,
+        ownerBytes: MemoryLayout<[Float]>.stride,
+        count: count,
+        elementBytes: MemoryLayout<Float>.stride,
+        label: "float32_array"
+    )
+    if hostIsLittleEndian {
+        return try readArrayUninitialized(count: count) { destination in
+            try context.buffer.readBytes(
+                into: UnsafeMutableRawBufferPointer(start: destination, count: byteSize))
+        }
+    }
+    return try readArrayUninitialized(count: count) { destination in
+        for index in 0..<count {
+            destination.advanced(by: index).initialize(to: try context.buffer.readFloat32())
+        }
+    }
+}
+
+@inline(__always)
+func readDoublePrimitiveArray(
+    _ context: ReadContext,
+    reserveGraphStorage: Bool = false
+) throws -> [Double] {
+    let byteSize = try readPrimitiveArrayByteSize(context)
+    if byteSize % 8 != 0 { throw primitiveArraySizeMismatch("float64") }
+    let count = byteSize / 8
+    try preparePrimitiveArrayStorage(
+        context,
+        reserveGraphStorage: reserveGraphStorage,
+        ownerBytes: MemoryLayout<[Double]>.stride,
+        count: count,
+        elementBytes: MemoryLayout<Double>.stride,
+        label: "float64_array"
+    )
+    if hostIsLittleEndian {
+        return try readArrayUninitialized(count: count) { destination in
+            try context.buffer.readBytes(
+                into: UnsafeMutableRawBufferPointer(start: destination, count: byteSize))
+        }
+    }
+    return try readArrayUninitialized(count: count) { destination in
+        for index in 0..<count {
+            destination.advanced(by: index).initialize(to: try context.buffer.readFloat64())
+        }
     }
 }
 
-// Keep the primitive type branches in one generic body so specialization removes every
-// nonmatching branch without adding forwarding calls to packed-array reads.
+// Common packed types use concrete readers so Array metadata and initialization stay specialized.
+// The generic entry delegates to the same readers to keep one decoding path per type.
 // swiftlint:disable:next function_body_length
 func readPrimitiveArray<Element: Serializer>(
     _ context: ReadContext,
     reserveGraphStorage: Bool = false
 ) throws -> [Element] {
-    let byteSize = Int(try context.buffer.readVarUInt32())
-    try context.ensureRemainingBytes(byteSize, label: "primitive_array_bytes")
+    if Element.self == Bool.self {
+        return uncheckedArrayCast(
+            try readBoolPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage),
+            to: Element.self)
+    }
+    if Element.self == Int32.self {
+        return uncheckedArrayCast(
+            try readInt32PrimitiveArray(context, reserveGraphStorage: reserveGraphStorage),
+            to: Element.self)
+    }
+    if Element.self == Int64.self {
+        return uncheckedArrayCast(
+            try readInt64PrimitiveArray(context, reserveGraphStorage: reserveGraphStorage),
+            to: Element.self)
+    }
+    if Element.self == Float.self {
+        return uncheckedArrayCast(
+            try readFloatPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage),
+            to: Element.self)
+    }
+    if Element.self == Double.self {
+        return uncheckedArrayCast(
+            try readDoublePrimitiveArray(context, reserveGraphStorage: reserveGraphStorage),
+            to: Element.self)
+    }
+
+    let byteSize = try readPrimitiveArrayByteSize(context)
 
     if Element.self == UInt8.self {
         try preparePrimitiveArray(
@@ -393,18 +581,6 @@ func readPrimitiveArray<Element: Serializer>(
         return uncheckedArrayCast(bytes, to: Element.self)
     }
 
-    if Element.self == Bool.self {
-        try preparePrimitiveArray(
-            context, reserveGraphStorage: reserveGraphStorage, type: Element.self, count: byteSize,
-            label: "bool_array")
-        let out = try readArrayUninitialized(count: byteSize) { destination in
-            for index in 0..<byteSize {
-                destination.advanced(by: index).initialize(to: try context.buffer.readUInt8() != 0)
-            }
-        }
-        return uncheckedArrayCast(out, to: Element.self)
-    }
-
     if Element.self == Int8.self {
         try preparePrimitiveArray(
             context, reserveGraphStorage: reserveGraphStorage, type: Element.self, count: byteSize,
@@ -437,27 +613,6 @@ func readPrimitiveArray<Element: Serializer>(
         return uncheckedArrayCast(out, to: Element.self)
     }
 
-    if Element.self == Int32.self {
-        if byteSize % 4 != 0 { throw primitiveArraySizeMismatch("int32") }
-        let count = byteSize / 4
-        try preparePrimitiveArray(
-            context, reserveGraphStorage: reserveGraphStorage, type: Element.self, count: count,
-            label: "int32_array")
-        if hostIsLittleEndian {
-            var out = Array(repeating: Int32(0), count: count)
-            try out.withUnsafeMutableBytes { rawBytes in
-                try context.buffer.readBytes(into: rawBytes)
-            }
-            return uncheckedArrayCast(out, to: Element.self)
-        }
-        let out = try readArrayUninitialized(count: count) { destination in
-            for index in 0..<count {
-                destination.advanced(by: index).initialize(to: try context.buffer.readInt32())
-            }
-        }
-        return uncheckedArrayCast(out, to: Element.self)
-    }
-
     if Element.self == UInt32.self {
         if byteSize % 4 != 0 { throw primitiveArraySizeMismatch("uint32") }
         let count = byteSize / 4
@@ -479,27 +634,6 @@ func readPrimitiveArray<Element: Serializer>(
         return uncheckedArrayCast(out, to: Element.self)
     }
 
-    if Element.self == Int64.self {
-        if byteSize % 8 != 0 { throw primitiveArraySizeMismatch("int64") }
-        let count = byteSize / 8
-        try preparePrimitiveArray(
-            context, reserveGraphStorage: reserveGraphStorage, type: Element.self, count: count,
-            label: "int64_array")
-        if hostIsLittleEndian {
-            var out = Array(repeating: Int64(0), count: count)
-            try out.withUnsafeMutableBytes { rawBytes in
-                try context.buffer.readBytes(into: rawBytes)
-            }
-            return uncheckedArrayCast(out, to: Element.self)
-        }
-        let out = try readArrayUninitialized(count: count) { destination in
-            for index in 0..<count {
-                destination.advanced(by: index).initialize(to: try context.buffer.readInt64())
-            }
-        }
-        return uncheckedArrayCast(out, to: Element.self)
-    }
-
     if Element.self == UInt64.self {
         if byteSize % 8 != 0 { throw primitiveArraySizeMismatch("uint64") }
         let count = byteSize / 8
@@ -572,45 +706,7 @@ func readPrimitiveArray<Element: Serializer>(
         return uncheckedArrayCast(values, to: Element.self)
     }
 
-    if Element.self == Float.self {
-        if byteSize % 4 != 0 { throw primitiveArraySizeMismatch("float32") }
-        let count = byteSize / 4
-        try preparePrimitiveArray(
-            context, reserveGraphStorage: reserveGraphStorage, type: Element.self, count: count,
-            label: "float32_array")
-        if hostIsLittleEndian {
-            var out = Array(repeating: Float(0), count: count)
-            try out.withUnsafeMutableBytes { rawBytes in
-                try context.buffer.readBytes(into: rawBytes)
-            }
-            return uncheckedArrayCast(out, to: Element.self)
-        }
-        let out = try readArrayUninitialized(count: count) { destination in
-            for index in 0..<count {
-                destination.advanced(by: index).initialize(to: try context.buffer.readFloat32())
-            }
-        }
-        return uncheckedArrayCast(out, to: Element.self)
-    }
-
-    if byteSize % 8 != 0 { throw primitiveArraySizeMismatch("float64") }
-    let count = byteSize / 8
-    try preparePrimitiveArray(
-        context, reserveGraphStorage: reserveGraphStorage, type: Element.self, count: count,
-        label: "float64_array")
-    if hostIsLittleEndian {
-        var out = Array(repeating: Double(0), count: count)
-        try out.withUnsafeMutableBytes { rawBytes in
-            try context.buffer.readBytes(into: rawBytes)
-        }
-        return uncheckedArrayCast(out, to: Element.self)
-    }
-    let out = try readArrayUninitialized(count: count) { destination in
-        for index in 0..<count {
-            destination.advanced(by: index).initialize(to: try context.buffer.readFloat64())
-        }
-    }
-    return uncheckedArrayCast(out, to: Element.self)
+    preconditionFailure("Unsupported primitive array element type \(Element.self)")
 }
 
 @inline(never)
@@ -805,6 +901,14 @@ public enum ArraySerializer<Element: Serializer>: Serializer {
             itemReadAlwaysAdvances: elementReadAlwaysAdvances,
             label: "array"
         )
+        if !trackRef && !hasNull && elementReadAlwaysAdvances {
+            if let elementTypeInfo {
+                return try Codec.withFieldTypeInfo(elementTypeInfo, context) {
+                    try readNonNullElements(context, codec: Codec.self, count: length)
+                }
+            }
+            return try readNonNullElements(context, codec: Codec.self, count: length)
+        }
         return try Codec.withFieldTypeInfo(elementTypeInfo, context) {
             if trackRef {
                 return try readArrayTrackingInitialization(
@@ -845,19 +949,6 @@ public enum ArraySerializer<Element: Serializer>: Serializer {
                 }
             }
 
-            if elementReadAlwaysAdvances {
-                return try readArrayTrackingInitialization(
-                    count: length
-                ) { destination, initializedCount in
-                    for index in 0..<length {
-                        destination.advanced(by: index).initialize(
-                            to: try Codec.readFieldData(context)
-                        )
-                        initializedCount = index + 1
-                    }
-                }
-            }
-
             return try readArrayTrackingInitialization(
                 count: length
             ) { destination, initializedCount in
@@ -883,6 +974,23 @@ public enum ArraySerializer<Element: Serializer>: Serializer {
             }
         }
     }
+
+    @inlinable
+    @inline(__always)
+    internal static func readNonNullElements<Codec: FieldCodec>(
+        _ context: ReadContext,
+        codec _: Codec.Type,
+        count: Int
+    ) throws -> [Codec.Target] where Codec.Target == Element.Target {
+        try [Codec.Target](unsafeUninitializedCapacity: count) { destination, initializedCount in
+            let baseAddress = destination.baseAddress!
+            for index in 0..<count {
+                baseAddress.advanced(by: index).initialize(to: try Codec.readFieldData(context))
+                // A later decode may throw, so Array must own every initialized prefix element.
+                initializedCount = index + 1
+            }
+        }
+    }
 }
 
 public extension ArraySerializer where Element: FieldCodec {
diff --git a/swift/Sources/Fory/FieldCodecs.swift b/swift/Sources/Fory/FieldCodecs.swift
index d2e9a3e74..c82f2f4ec 100644
--- a/swift/Sources/Fory/FieldCodecs.swift
+++ b/swift/Sources/Fory/FieldCodecs.swift
@@ -1402,7 +1402,7 @@ private func readPackedArrayPayload<ElementCodec: FieldCodec>(
 ) throws -> [ElementCodec.Target]? {
     if ElementCodec.self == BoolCodec.self {
         return uncheckedPackedArrayCast(
-            try readPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage) as [Bool],
+            try readBoolPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage),
             to: ElementCodec.Target.self)
     }
     if ElementCodec.self == Int8Codec.self {
@@ -1417,12 +1417,12 @@ private func readPackedArrayPayload<ElementCodec: FieldCodec>(
     }
     if ElementCodec.self == Int32FixedCodec.self {
         return uncheckedPackedArrayCast(
-            try readPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage) as [Int32],
+            try readInt32PrimitiveArray(context, reserveGraphStorage: reserveGraphStorage),
             to: ElementCodec.Target.self)
     }
     if ElementCodec.self == Int64FixedCodec.self {
         return uncheckedPackedArrayCast(
-            try readPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage) as [Int64],
+            try readInt64PrimitiveArray(context, reserveGraphStorage: reserveGraphStorage),
             to: ElementCodec.Target.self)
     }
     if ElementCodec.self == IntFixedCodec.self {
@@ -1467,12 +1467,12 @@ private func readPackedArrayPayload<ElementCodec: FieldCodec>(
     }
     if ElementCodec.self == FloatCodec.self {
         return uncheckedPackedArrayCast(
-            try readPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage) as [Float],
+            try readFloatPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage),
             to: ElementCodec.Target.self)
     }
     if ElementCodec.self == DoubleCodec.self {
         return uncheckedPackedArrayCast(
-            try readPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage) as [Double],
+            try readDoublePrimitiveArray(context, reserveGraphStorage: reserveGraphStorage),
             to: ElementCodec.Target.self)
     }
     return nil
diff --git a/swift/Sources/Fory/TypeMeta.swift b/swift/Sources/Fory/TypeMeta.swift
index 29e9efde0..da093c1c2 100644
--- a/swift/Sources/Fory/TypeMeta.swift
+++ b/swift/Sources/Fory/TypeMeta.swift
@@ -314,6 +314,8 @@ public final class TypeMeta: Equatable, @unchecked Sendable {
     public let registerByName: Bool
     public let fields: [FieldInfo]
     public let compressed: Bool
+    /// The protocol-defined 52-bit schema identity. Equal values identify the same schema,
+    /// so readers do not need to compare the field array again.
     public let headerHash: UInt64
 
     internal var readDataAlwaysAdvances: Bool {
diff --git a/swift/Sources/ForyMacro/ForyObjectMacroReadGeneration.swift b/swift/Sources/ForyMacro/ForyObjectMacroReadGeneration.swift
index f533ae257..55ce7e47d 100644
--- a/swift/Sources/ForyMacro/ForyObjectMacroReadGeneration.swift
+++ b/swift/Sources/ForyMacro/ForyObjectMacroReadGeneration.swift
@@ -345,10 +345,8 @@ private func buildClassReadCompatibleDataDecl(
             if let reservedRefID {
                 context.refReader.storeRef(value, at: reservedRefID)
             }
-            if let localTypeMeta = remoteTypeInfo.typeMeta,
-               let localHeaderHash = remoteTypeInfo.typeDefHeaderHash,
-               typeMeta.headerHash == localHeaderHash,
-               typeMeta.fields == localTypeMeta.fields {
+            if let localHeaderHash = remoteTypeInfo.typeDefHeaderHash,
+               typeMeta.headerHash == localHeaderHash {
                 if !remoteTypeInfo.typeDefHasUserTypeFields {
                     \(schemaAssignBody)
                     return value
@@ -382,10 +380,8 @@ private func buildEmptyStructReadCompatibleDataDecl(accessPrefix: String) -> Str
         guard let typeMeta = typeInfo.compatibleTypeMeta else {
             throw ForyError.invalidData("compatible type metadata is required")
         }
-        if let localTypeMeta = typeInfo.typeMeta,
-           let localHeaderHash = typeInfo.typeDefHeaderHash,
-           typeMeta.headerHash == localHeaderHash,
-           typeMeta.fields == localTypeMeta.fields {
+        if let localHeaderHash = typeInfo.typeDefHeaderHash,
+           typeMeta.headerHash == localHeaderHash {
             return Target()
         }
         for remoteField in typeMeta.fields {
@@ -436,10 +432,8 @@ private func buildStructReadCompatibleDataDecl(
             \(bufferBinding)guard let typeMeta = typeInfo.compatibleTypeMeta else {
                 throw ForyError.invalidData("compatible type metadata is required")
             }
-            if let localTypeMeta = typeInfo.typeMeta,
-               let localHeaderHash = typeInfo.typeDefHeaderHash,
-               typeMeta.headerHash == localHeaderHash,
-               typeMeta.fields == localTypeMeta.fields {
+            if let localHeaderHash = typeInfo.typeDefHeaderHash,
+               typeMeta.headerHash == localHeaderHash {
                 if !typeInfo.typeDefHasUserTypeFields {
                     \(schemaReadBody)
                     return Target(


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@fory.apache.org
For additional commands, e-mail: commits-help@fory.apache.org