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

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new 897ece64 [PERF][EXTRA] Split lazy structural mutation loops (#756)
897ece64 is described below

commit 897ece64d6ad0857f803e68221375021867e81a5
Author: Tianqi Chen <[email protected]>
AuthorDate: Sun Sep 6 14:38:26 2026 -0400

    [PERF][EXTRA] Split lazy structural mutation loops (#756)
    
    The ordinary sequence and map structural mutation paths currently keep
    their lazy-output null check inside the per-element loop, even after the
    first changed child has allocated the output. Split each path into an
    unchanged-prefix scan and a post-allocation suffix loop so the suffix
    does strictly less bookkeeping.
    
    The no-change return, prefix copy, stable map iterator alignment,
    copy-on-write behavior, and error propagation remain unchanged. A
    focused regression covers retained arrays and maps with unchanged and
    changed suffix elements, plus an error after allocation.
---
 src/ffi/extra/structural_mutate.cc        | 105 ++++++++++++++++++++----------
 tests/cpp/extra/test_structural_mutate.cc |  37 +++++++++++
 2 files changed, 106 insertions(+), 36 deletions(-)

diff --git a/src/ffi/extra/structural_mutate.cc 
b/src/ffi/extra/structural_mutate.cc
index 71511a94..6060fb5e 100644
--- a/src/ffi/extra/structural_mutate.cc
+++ b/src/ffi/extra/structural_mutate.cc
@@ -149,6 +149,34 @@ Expected<Any> StructuralMutateExpected(
 // Built-in container structural mutation.
 // ---------------------------------------------------------------------------
 
+/*!
+ * \brief Finish mutating a sequence after its first changed element.
+ *
+ * \tparam SeqObj The underlying sequence object type.
+ * \param mutator The active structural mutator.
+ * \param self The source sequence object.
+ * \param index The index of the first changed element.
+ * \param first The mapped value for the first changed element.
+ * \return The mutated sequence, or an Error.
+ */
+template <typename SeqObj>
+TVM_FFI_INLINE TVMFFIAny MutateSeqContainerChanged(StructuralMutatorObj* 
mutator,
+                                                   const SeqObj* self, int64_t 
index,
+                                                   Any first) noexcept {
+  int64_t size = static_cast<int64_t>(self->size());
+  const Any* items = self->begin();
+  ObjectPtr<SeqObj> output = SeqObj::CreateRepeated(size, Any());
+  output->InitRange(0, items, items + index);
+  output->SetItemAfterCheck(index, std::move(first));
+
+  for (int64_t i = index + 1; i < size; ++i) {
+    const Any& item = items[i];
+    TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, mapped_value, 
mutator->MutateExpected(item));
+    output->SetItemAfterCheck(i, std::move(mapped_value));
+  }
+  return AnyUnsafe::MoveAnyToTVMFFIAny(Any(std::move(output)));
+}
+
 /*!
  * \brief Structurally mutate the elements of a sequence container.
  *
@@ -163,26 +191,15 @@ TVMFFIAny MutateSeqContainerRaw(StructuralMutatorObj* 
mutator, AnyView value,
                                 const SeqObj* self) noexcept {
   int64_t size = static_cast<int64_t>(self->size());
   const Any* items = self->begin();
-  ObjectPtr<SeqObj> output = nullptr;
 
   for (int64_t i = 0; i < size; ++i) {
     const Any& item = items[i];
     TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, mapped_value, 
mutator->MutateExpected(item));
-
-    if (output == nullptr) {
-      if (item.same_as(mapped_value)) {
-        continue;
-      }
-      output = SeqObj::CreateRepeated(size, Any());
-      output->InitRange(0, items, items + i);
+    if (!item.same_as(mapped_value)) {
+      return MutateSeqContainerChanged(mutator, self, i, 
std::move(mapped_value));
     }
-    output->SetItemAfterCheck(i, std::move(mapped_value));
   }
-
-  if (output == nullptr) {
-    return AnyUnsafe::MoveAnyToTVMFFIAny(Any(value));
-  }
-  return AnyUnsafe::MoveAnyToTVMFFIAny(Any(std::move(output)));
+  return AnyUnsafe::MoveAnyToTVMFFIAny(Any(value));
 }
 
 /*!
@@ -209,6 +226,41 @@ TVMFFIAny 
MaybeInplaceMutateSeqContainerRaw(StructuralMutatorObj* mutator, AnyVi
   return AnyUnsafe::MoveAnyToTVMFFIAny(Any(value));
 }
 
+/*!
+ * \brief Finish mutating map values after the first changed value.
+ *
+ * \tparam MapObjType The underlying map object type.
+ * \param mutator The active structural mutator.
+ * \param self The source map object.
+ * \param source_it Iterator at the first changed value.
+ * \param index Iteration index of the first changed value.
+ * \param first The mapped value for the first changed entry.
+ * \return The mutated map, or an Error.
+ */
+template <typename MapObjType>
+TVM_FFI_INLINE TVMFFIAny MutateMapValuesChanged(StructuralMutatorObj* mutator,
+                                                const MapObjType* self,
+                                                MapBaseObj::iterator 
source_it, size_t index,
+                                                Any first) noexcept {
+  ObjectPtr<Object> output = MapObjType::ShallowCopy(self);
+  auto output_it = static_cast<MapBaseObj*>(output.get())->begin();
+  for (size_t i = 0; i < index; ++i) {
+    ++output_it;
+  }
+  output_it->second = std::move(first);
+  ++source_it;
+  ++output_it;
+
+  for (; source_it != self->end(); ++source_it, ++output_it) {
+    const Any& old_value = source_it->second;
+    TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, new_value, 
mutator->MutateExpected(old_value));
+    if (!old_value.same_as(new_value)) {
+      output_it->second = std::move(new_value);
+    }
+  }
+  return AnyUnsafe::MoveAnyToTVMFFIAny(Any(std::move(output)));
+}
+
 /*!
  * \brief Structurally mutate the values of a map container.
  *
@@ -221,34 +273,15 @@ TVMFFIAny 
MaybeInplaceMutateSeqContainerRaw(StructuralMutatorObj* mutator, AnyVi
 template <typename MapObjType>
 TVMFFIAny MutateMapValuesRaw(StructuralMutatorObj* mutator, AnyView value,
                              const MapObjType* self) noexcept {
-  ObjectPtr<Object> output = nullptr;
-  MapBaseObj::iterator output_it;
   size_t index = 0;
-
   for (auto source_it = self->begin(); source_it != self->end(); ++source_it, 
++index) {
     const Any& old_value = source_it->second;
     TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, new_value, 
mutator->MutateExpected(old_value));
-    bool changed = !old_value.same_as(new_value);
-    if (output == nullptr) {
-      if (!changed) {
-        continue;
-      }
-      output = MapObjType::ShallowCopy(self);
-      output_it = static_cast<MapBaseObj*>(output.get())->begin();
-      for (size_t i = 0; i < index; ++i) {
-        ++output_it;
-      }
-    }
-    if (changed) {
-      output_it->second = std::move(new_value);
+    if (!old_value.same_as(new_value)) {
+      return MutateMapValuesChanged(mutator, self, source_it, index, 
std::move(new_value));
     }
-    ++output_it;
-  }
-
-  if (output == nullptr) {
-    return AnyUnsafe::MoveAnyToTVMFFIAny(Any(value));
   }
-  return AnyUnsafe::MoveAnyToTVMFFIAny(Any(std::move(output)));
+  return AnyUnsafe::MoveAnyToTVMFFIAny(Any(value));
 }
 
 /*!
diff --git a/tests/cpp/extra/test_structural_mutate.cc 
b/tests/cpp/extra/test_structural_mutate.cc
index 0170c45d..b99b0a3c 100644
--- a/tests/cpp/extra/test_structural_mutate.cc
+++ b/tests/cpp/extra/test_structural_mutate.cc
@@ -560,6 +560,43 @@ TEST(StructuralMap, PreservesSharedArrayAndMapInputs) {
   }
 }
 
+TEST(StructuralMap, CopyOnWriteProcessesSuffixAfterFirstChange) {
+  AnyArray array_root{String("prefix"), int64_t{1}, String("middle"), 
int64_t{2}};
+  AnyArray array_owner = array_root;  // 
NOLINT(performance-unnecessary-copy-initialization)
+  AnyArray mapped_array =
+      StructuralMap<WalkOrder::kPostOrder>(array_root, 
Increment).cast<AnyArray>();
+
+  EXPECT_TRUE(mapped_array[0].same_as(array_root[0]));
+  EXPECT_EQ(mapped_array[1].cast<int64_t>(), 2);
+  EXPECT_TRUE(mapped_array[2].same_as(array_root[2]));
+  EXPECT_EQ(mapped_array[3].cast<int64_t>(), 3);
+  EXPECT_TRUE(array_owner.same_as(array_root));
+
+  StringMap map_root{{"prefix", String("unchanged")},
+                     {"first", int64_t{1}},
+                     {"middle", String("also-unchanged")},
+                     {"second", int64_t{2}}};
+  StringMap map_owner = map_root;  // 
NOLINT(performance-unnecessary-copy-initialization)
+  StringMap mapped_map =
+      StructuralMap<WalkOrder::kPostOrder>(map_root, 
Increment).cast<StringMap>();
+
+  EXPECT_TRUE(mapped_map["prefix"].same_as(map_root["prefix"]));
+  EXPECT_EQ(mapped_map["first"].cast<int64_t>(), 2);
+  EXPECT_TRUE(mapped_map["middle"].same_as(map_root["middle"]));
+  EXPECT_EQ(mapped_map["second"].cast<int64_t>(), 3);
+  EXPECT_TRUE(map_owner.same_as(map_root));
+
+  Expected<Any> suffix_error = StructuralMapExpected<WalkOrder::kPostOrder>(
+      AnyArray{int64_t{1}, int64_t{2}}, [](int64_t value) -> Expected<Any> {
+        if (value == 2) {
+          return Unexpected(Error("ValueError", "suffix mutate failed", ""));
+        }
+        return Any(value + 1);
+      });
+  ASSERT_TRUE(suffix_error.is_err());
+  EXPECT_EQ(suffix_error.error().message(), "suffix mutate failed");
+}
+
 TEST(StructuralMap, PreOrderRecursivelyMapsCallbackResult) {
   StringMap root{{"value", AnyArray{int64_t{1}}}};
   AnyArray replacement{int64_t{10}};

Reply via email to