Store primitive GC arrays as raw byte buffers (#9051)

GCData previously represented all allocations using a vector of
Literals. Storing numeric array elements this way introduces unnecessary
memory overhead and prevents efficient byte-level operations.

Represent primitive numeric GC arrays using a raw byte buffer in GCData
while preserving Literals storage for reference arrays and structs.
diff --git a/src/literal.h b/src/literal.h
index adb8e2f..335e117 100644
--- a/src/literal.h
+++ b/src/literal.h
@@ -19,6 +19,7 @@
 
 #include <array>
 #include <iostream>
+#include <variant>
 
 #include "support/bits.h"
 #include "support/hash.h"
@@ -212,7 +213,7 @@
     }
   }
 
-  static Literal makeFromMemory(void* p, Type type);
+  static Literal makeFromMemory(const void* p, Type type);
 
   static Literal makeSignedMin(Type type) {
     switch (type.getBasic()) {
@@ -312,6 +313,12 @@
   Name getFunc() const;
   std::shared_ptr<FuncData> getFuncData() const;
   std::shared_ptr<GCData> getGCData() const;
+  size_t getNumElements() const;
+  Literal getElement(size_t index, bool signed_ = false) const;
+  void setElement(size_t index, Literal value);
+  bool isRawBytes() const;
+  const std::vector<uint8_t>& getRawBytes() const;
+  std::vector<uint8_t>& getRawBytes();
   std::shared_ptr<ExnData> getExnData() const;
   std::shared_ptr<ContData> getContData() const;
 
@@ -784,28 +791,65 @@
 // A GC Struct, Array, or String is a set of values with a type saying how it
 // should be interpreted.
 struct GCData {
-  // The element or field values.
-  Literals values;
+  // The element or field values. Primitive numeric arrays use raw byte buffers
+  // (std::vector<uint8_t>), while reference arrays, structs, strings, and other
+  // reference allocations use Literals.
+  std::variant<std::vector<uint8_t>, Literals> storage;
 
   // The descriptor, if it exists, or null.
   Literal desc;
 
   GCData(Literals&& values,
          const Literal& desc = Literal::makeNull(HeapType::none))
-    : values(std::move(values)), desc(desc) {}
+    : storage(std::move(values)), desc(desc) {}
+
+  GCData(std::vector<uint8_t>&& data,
+         const Literal& desc = Literal::makeNull(HeapType::none))
+    : storage(std::move(data)), desc(desc) {}
+
+  bool isRawBytes() const {
+    return std::holds_alternative<std::vector<uint8_t>>(storage);
+  }
+
+  const std::vector<uint8_t>& getRawBytes() const {
+    return std::get<std::vector<uint8_t>>(storage);
+  }
+
+  std::vector<uint8_t>& getRawBytes() {
+    return std::get<std::vector<uint8_t>>(storage);
+  }
+
+  const Literals& getLiterals() const { return std::get<Literals>(storage); }
+
+  Literals& getLiterals() { return std::get<Literals>(storage); }
 };
 
+inline bool Literal::isRawBytes() const {
+  assert(isData());
+  return gcData->isRawBytes();
+}
+
+inline const std::vector<uint8_t>& Literal::getRawBytes() const {
+  assert(isData());
+  return gcData->getRawBytes();
+}
+
+inline std::vector<uint8_t>& Literal::getRawBytes() {
+  assert(isData());
+  return gcData->getRawBytes();
+}
+
 inline bool Literal::hasExternPayload() const {
   if (isNull()) {
     return false;
   }
   assert(type.getHeapType().isMaybeShared(HeapType::ext));
-  return gcData->values[0].type == Type::i32;
+  return gcData->getLiterals()[0].type == Type::i32;
 }
 
 inline int32_t Literal::getExternPayload() const {
   assert(hasExternPayload());
-  return gcData->values[0].geti32();
+  return gcData->getLiterals()[0].geti32();
 }
 
 } // namespace wasm
@@ -869,7 +913,7 @@
         return digest;
       }
       if (a.type.isString()) {
-        auto& values = a.getGCData()->values;
+        auto& values = a.getGCData()->getLiterals();
         wasm::rehash(digest, values.size());
         for (auto c : values) {
           wasm::rehash(digest, c.getInteger());
diff --git a/src/passes/Precompute.cpp b/src/passes/Precompute.cpp
index 8f550bd..a1357e6 100644
--- a/src/passes/Precompute.cpp
+++ b/src/passes/Precompute.cpp
@@ -1103,7 +1103,7 @@
   // string.
   bool isValidUTF16Literal(const Literal& value) {
     bool expectLowSurrogate = false;
-    for (auto& v : value.getGCData()->values) {
+    for (auto& v : value.getGCData()->getLiterals()) {
       auto c = v.getInteger();
       if (c >= 0xDC00 && c <= 0xDFFF) {
         if (expectLowSurrogate) {
diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp
index f5b05c9..9025832 100644
--- a/src/tools/wasm-ctor-eval.cpp
+++ b/src/tools/wasm-ctor-eval.cpp
@@ -912,7 +912,6 @@
     } else {
       // This is the first usage of this data. Generate a struct.new /
       // array.new for it.
-      auto& values = data->values;
       std::vector<Expression*> args;
 
       // The initial values for this allocation may themselves be GC
@@ -934,8 +933,8 @@
         definingGlobals[data] = DefiningGlobalInfo{definingGlobalName, type};
       }
 
-      for (auto& value : values) {
-        auto* serialized = getSerialization(value);
+      for (size_t i = 0; i < value.getNumElements(); i++) {
+        auto* serialized = getSerialization(value.getElement(i));
         if (!serialized) {
           return nullptr;
         }
diff --git a/src/wasm-builder.h b/src/wasm-builder.h
index d060dd6..56db519 100644
--- a/src/wasm-builder.h
+++ b/src/wasm-builder.h
@@ -1511,7 +1511,7 @@
       // The string is already WTF-16, but we need to convert from `Literals` to
       // actual string.
       std::stringstream wtf16;
-      for (auto c : value.getGCData()->values) {
+      for (auto c : value.getGCData()->getLiterals()) {
         auto u = c.getInteger();
         assert(u < 0x10000);
         wtf16 << uint8_t(u & 0xFF);
diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h
index 56fccae..fbf63a9 100644
--- a/src/wasm-interpreter.h
+++ b/src/wasm-interpreter.h
@@ -370,10 +370,11 @@
   // this function in LSan.
   //
   // This consumes the input |data| entirely.
-  Literal makeGCData(Literals&& data,
+  template<typename T>
+  Literal makeGCData(T&& data,
                      Type type,
                      Literal desc = Literal::makeNull(HeapType::none)) {
-    auto allocation = std::make_shared<GCData>(std::move(data), desc);
+    auto allocation = std::make_shared<GCData>(std::forward<T>(data), desc);
 #if __has_feature(leak_sanitizer) || __has_feature(address_sanitizer)
     // GC data with cycles will leak, since shared_ptrs do not handle cycles.
     // Binaryen is generally not used in long-running programs so we just ignore
@@ -384,6 +385,23 @@
     return Literal(allocation, type.getHeapType());
   }
 
+  Literal makeGCArray(Type type,
+                      size_t num,
+                      Literal desc = Literal::makeNull(HeapType::none)) {
+    auto heapType = type.getHeapType();
+    const auto& element = heapType.getArray().element;
+    if (element.type.isRef()) {
+      Literals data(num);
+      auto zero = Literal::makeZero(element.type);
+      for (size_t i = 0; i < num; ++i) {
+        data[i] = zero;
+      }
+      return makeGCData(std::move(data), type, desc);
+    }
+    size_t elemBytes = element.getByteSize();
+    return makeGCData(std::vector<uint8_t>(num * elemBytes, 0), type, desc);
+  }
+
   // Same as makeGCData but for ExnData.
   Literal makeExnData(Tag* tag, const Literals& payload) {
     auto allocation = std::make_shared<ExnData>(tag, payload);
@@ -393,20 +411,6 @@
     return Literal(allocation);
   }
 
-  template<typename T>
-  void writeBytes(T value, int numBytes, size_t index, Literals& values) {
-    if constexpr (std::is_same_v<T, std::array<uint8_t, 16>>) {
-      for (int i = 0; i < numBytes; ++i) {
-        values[index + i] = Literal(static_cast<int32_t>(value[i]));
-      }
-    } else {
-      for (int i = 0; i < numBytes; ++i) {
-        values[index + i] =
-          Literal(static_cast<int32_t>((value >> (i * 8)) & 0xff));
-      }
-    }
-  }
-
   static Literal applyRMW(AtomicRMWOp op, Literal lhs, Literal rhs) {
     switch (op) {
       case RMWAdd:
@@ -2276,7 +2280,8 @@
       trap("null ref");
     }
     auto field = curr->ref->type.getHeapType().getStruct().fields[curr->index];
-    return extendForPacking(data->values[curr->index], field, curr->signed_);
+    return extendForPacking(
+      data->getLiterals()[curr->index], field, curr->signed_);
   }
   Flow visitStructSet(StructSet* curr) {
     VISIT(ref, curr->ref)
@@ -2286,7 +2291,7 @@
       trap("null ref");
     }
     auto field = curr->ref->type.getHeapType().getStruct().fields[curr->index];
-    data->values[curr->index] =
+    data->getLiterals()[curr->index] =
       truncateForPacking(value.getSingleValue(), field);
     return Flow();
   }
@@ -2298,7 +2303,7 @@
     if (!data) {
       trap("null ref");
     }
-    auto& field = data->values[curr->index];
+    auto& field = data->getLiterals()[curr->index];
     auto oldVal = field;
     field = applyRMW(curr->op, oldVal, value.getSingleValue());
     return oldVal;
@@ -2312,7 +2317,7 @@
     if (!data) {
       trap("null ref");
     }
-    auto& field = data->values[curr->index];
+    auto& field = data->getLiterals()[curr->index];
     auto oldVal = field;
     if (field == expected.getSingleValue()) {
       field = replacement.getSingleValue();
@@ -2337,7 +2342,7 @@
     if (!waitqueue.getSingleValue().getGCData()) {
       trap("null ref");
     }
-    auto& field = data->values[curr->index];
+    auto& field = data->getLiterals()[curr->index];
     if (field != expected.getSingleValue()) {
       return Literal(int32_t{1}); // not equal
     }
@@ -2354,7 +2359,7 @@
   }
 
   Flow visitWaitqueueNew(WaitqueueNew* curr) {
-    return self()->makeGCData({},
+    return self()->makeGCData(Literals{},
                               Type(HeapTypes::sharedWaitqueue, NonNullable));
   }
 
@@ -2394,26 +2399,18 @@
       assert(init.breaking());
       return init;
     }
-    auto heapType = curr->type.getHeapType();
-    const auto& element = heapType.getArray().element;
     Index num = size.getSingleValue().geti32();
     if (num >= DataLimit) {
       hostLimit("allocation failure");
     }
-    Literals data(num);
-    if (curr->isWithDefault()) {
-      auto zero = Literal::makeZero(element.type);
+    auto ret = makeGCArray(curr->type, num);
+    if (!curr->isWithDefault()) {
+      Literal val = init.getSingleValue();
       for (Index i = 0; i < num; i++) {
-        data[i] = zero;
-      }
-    } else {
-      auto field = curr->type.getHeapType().getArray().element;
-      auto value = truncateForPacking(init.getSingleValue(), field);
-      for (Index i = 0; i < num; i++) {
-        data[i] = value;
+        ret.setElement(i, val);
       }
     }
-    return makeGCData(std::move(data), curr->type);
+    return ret;
   }
   Flow visitArrayNewData(ArrayNewData* curr) { WASM_UNREACHABLE("unimp"); }
   Flow visitArrayNewElem(ArrayNewElem* curr) { WASM_UNREACHABLE("unimp"); }
@@ -2430,91 +2427,105 @@
       }
       WASM_UNREACHABLE("unreachable but no unreachable child");
     }
-    auto heapType = curr->type.getHeapType();
-    auto field = heapType.getArray().element;
-    Literals data(num);
+    auto ret = makeGCArray(curr->type, num);
     for (Index i = 0; i < num; i++) {
       VISIT(value, curr->values[i])
-      data[i] = truncateForPacking(value.getSingleValue(), field);
+      ret.setElement(i, value.getSingleValue());
     }
-    return makeGCData(std::move(data), curr->type);
+    return ret;
   }
   Flow visitArrayGet(ArrayGet* curr) {
     VISIT(ref, curr->ref)
     VISIT(index, curr->index)
-    auto data = ref.getSingleValue().getGCData();
-    if (!data) {
+    auto refVal = ref.getSingleValue();
+    if (refVal.isNull()) {
       trap("null ref");
     }
     Index i = index.getSingleValue().geti32();
-    if (i >= data->values.size()) {
+    if (i >= refVal.getNumElements()) {
       trap("array oob");
     }
-    auto field = curr->ref->type.getHeapType().getArray().element;
-    return extendForPacking(data->values[i], field, curr->signed_);
+    return refVal.getElement(i, curr->signed_);
   }
   Flow visitArraySet(ArraySet* curr) {
     VISIT(ref, curr->ref)
     VISIT(index, curr->index)
     VISIT(value, curr->value)
-    auto data = ref.getSingleValue().getGCData();
-    if (!data) {
+    auto refVal = ref.getSingleValue();
+    if (refVal.isNull()) {
       trap("null ref");
     }
     Index i = index.getSingleValue().geti32();
-    if (i >= data->values.size()) {
+    if (i >= refVal.getNumElements()) {
       trap("array oob");
     }
-    auto field = curr->ref->type.getHeapType().getArray().element;
-    data->values[i] = truncateForPacking(value.getSingleValue(), field);
+    refVal.setElement(i, value.getSingleValue());
     return Flow();
   }
   Flow visitArrayLoad(ArrayLoad* curr) {
     VISIT(ref, curr->ref)
     VISIT(index, curr->index)
-    auto data = ref.getSingleValue().getGCData();
-    if (!data) {
+    auto refVal = ref.getSingleValue();
+    if (refVal.isNull()) {
       trap("null ref");
     }
     Index i = index.getSingleValue().geti32();
-    size_t size = data->values.size();
+    size_t size = refVal.getRawBytes().size();
     if (i >= size || curr->bytes > (size - i)) {
       trap("array oob");
     }
-    uint64_t val = 0;
-    for (unsigned b = 0; b < curr->bytes; ++b) {
-      val |= static_cast<uint64_t>(data->values[i + b].geti32()) << (b * 8);
-    }
+    const uint8_t* p = &refVal.getRawBytes()[i];
     switch (curr->type.getBasic()) {
       case Type::i32: {
-        int32_t sval = static_cast<int32_t>(val);
-        if (curr->signed_) {
-          if (curr->bytes == 1) {
-            sval = static_cast<int32_t>(static_cast<int8_t>(sval));
-          } else if (curr->bytes == 2) {
-            sval = static_cast<int32_t>(static_cast<int16_t>(sval));
-          }
+        switch (curr->bytes) {
+          case 1:
+            return curr->signed_ ? Literal((int32_t)Bits::readLE<int8_t>(p))
+                                 : Literal((int32_t)Bits::readLE<uint8_t>(p));
+          case 2:
+            return curr->signed_ ? Literal((int32_t)Bits::readLE<int16_t>(p))
+                                 : Literal((int32_t)Bits::readLE<uint16_t>(p));
+          case 4:
+            return Literal(Bits::readLE<int32_t>(p));
+          default:
+            WASM_UNREACHABLE("invalid size");
         }
-        return Literal(sval);
       }
       case Type::i64: {
-        int64_t sval = static_cast<int64_t>(val);
-        if (curr->signed_) {
-          if (curr->bytes == 1) {
-            sval = static_cast<int64_t>(static_cast<int8_t>(sval));
-          } else if (curr->bytes == 2) {
-            sval = static_cast<int64_t>(static_cast<int16_t>(sval));
-          } else if (curr->bytes == 4) {
-            sval = static_cast<int64_t>(static_cast<int32_t>(sval));
-          }
+        switch (curr->bytes) {
+          case 1:
+            return curr->signed_ ? Literal((int64_t)Bits::readLE<int8_t>(p))
+                                 : Literal((int64_t)Bits::readLE<uint8_t>(p));
+          case 2:
+            return curr->signed_ ? Literal((int64_t)Bits::readLE<int16_t>(p))
+                                 : Literal((int64_t)Bits::readLE<uint16_t>(p));
+          case 4:
+            return curr->signed_ ? Literal((int64_t)Bits::readLE<int32_t>(p))
+                                 : Literal((int64_t)Bits::readLE<uint32_t>(p));
+          case 8:
+            return Literal(Bits::readLE<int64_t>(p));
+          default:
+            WASM_UNREACHABLE("invalid size");
         }
-        return Literal(sval);
       }
       case Type::f32: {
-        return Literal(bit_cast<float>(static_cast<int32_t>(val)));
+        switch (curr->bytes) {
+          case 2:
+            return Literal(bit_cast<int32_t>(fp16_ieee_to_fp32_value(
+                             Bits::readLE<uint16_t>(p))))
+              .castToF32();
+          case 4:
+            return Literal(Bits::readLE<uint32_t>(p)).castToF32();
+          default:
+            WASM_UNREACHABLE("invalid size");
+        }
       }
       case Type::f64: {
-        return Literal(bit_cast<double>(static_cast<int64_t>(val)));
+        assert(curr->bytes == 8);
+        return Literal(Bits::readLE<uint64_t>(p)).castToF64();
+      }
+      case Type::v128: {
+        assert(curr->bytes == 16);
+        return Literal(p);
       }
       default:
         WASM_UNREACHABLE("invalid type");
@@ -2525,55 +2536,36 @@
     VISIT(ref, curr->ref)
     VISIT(index, curr->index)
     VISIT(value, curr->value)
-    auto data = ref.getSingleValue().getGCData();
-    if (!data) {
+    auto refVal = ref.getSingleValue();
+    if (refVal.isNull()) {
       trap("null ref");
     }
 
     Index i = index.getSingleValue().geti32();
-    size_t size = data->values.size();
+    size_t size = refVal.getRawBytes().size();
     // Use subtraction to avoid overflow.
     if (i >= size || curr->bytes > (size - i)) {
       trap("array oob");
     }
-    switch (curr->value->type.getBasic()) {
-      case Type::i32:
-        writeBytes(
-          value.getSingleValue().geti32(), curr->bytes, i, data->values);
-        break;
-      case Type::i64:
-        writeBytes(
-          value.getSingleValue().geti64(), curr->bytes, i, data->values);
-        break;
-      case Type::f32:
-        writeBytes(value.getSingleValue().reinterpreti32(),
-                   curr->bytes,
-                   i,
-                   data->values);
-        break;
-      case Type::f64:
-        writeBytes(value.getSingleValue().reinterpreti64(),
-                   curr->bytes,
-                   i,
-                   data->values);
-        break;
-      case Type::v128:
-        writeBytes(
-          value.getSingleValue().getv128(), curr->bytes, i, data->values);
-        break;
-      case Type::none:
-      case Type::unreachable:
-        WASM_UNREACHABLE("unimp basic type");
+    uint8_t* p = &refVal.getRawBytes()[i];
+    auto val = value.getSingleValue();
+    if (curr->value->type == Type::f32 && curr->bytes == 2) {
+      float f32 = bit_cast<float>(val.reinterpreti32());
+      Bits::writeLE<uint16_t>(fp16_ieee_from_fp32_value(f32), p);
+    } else {
+      uint8_t buf[16];
+      val.getBits(buf);
+      memcpy(p, buf, curr->bytes);
     }
     return Flow();
   }
   Flow visitArrayLen(ArrayLen* curr) {
     VISIT(ref, curr->ref)
-    auto data = ref.getSingleValue().getGCData();
-    if (!data) {
+    auto refVal = ref.getSingleValue();
+    if (refVal.isNull()) {
       trap("null ref");
     }
-    return Literal(int32_t(data->values.size()));
+    return Literal(int32_t(refVal.getNumElements()));
   }
   Flow visitArrayCopy(ArrayCopy* curr) {
     VISIT(destRef, curr->destRef)
@@ -2581,30 +2573,42 @@
     VISIT(srcRef, curr->srcRef)
     VISIT(srcIndex, curr->srcIndex)
     VISIT(length, curr->length)
-    auto destData = destRef.getSingleValue().getGCData();
-    if (!destData) {
+    auto destVal = destRef.getSingleValue();
+    if (destVal.isNull()) {
       trap("null ref");
     }
-    auto srcData = srcRef.getSingleValue().getGCData();
-    if (!srcData) {
+    auto srcVal = srcRef.getSingleValue();
+    if (srcVal.isNull()) {
       trap("null ref");
     }
-    size_t destVal = destIndex.getSingleValue().getUnsigned();
-    size_t srcVal = srcIndex.getSingleValue().getUnsigned();
+    size_t destIdx = destIndex.getSingleValue().getUnsigned();
+    size_t srcIdx = srcIndex.getSingleValue().getUnsigned();
     size_t lengthVal = length.getSingleValue().getUnsigned();
-    if (destVal + lengthVal > destData->values.size()) {
+    if (destIdx + lengthVal > destVal.getNumElements()) {
       trap("oob");
     }
-    if (srcVal + lengthVal > srcData->values.size()) {
+    if (srcIdx + lengthVal > srcVal.getNumElements()) {
       trap("oob");
     }
+    // Fast path: bulk copy raw byte buffers directly with memmove rather than
+    // extracting and setting elements one-by-one via Literals.
+    if (destVal.isRawBytes() && srcVal.isRawBytes()) {
+      if (lengthVal > 0) {
+        auto elemSize =
+          destVal.type.getHeapType().getArray().element.getByteSize();
+        memmove(&destVal.getRawBytes()[destIdx * elemSize],
+                &srcVal.getRawBytes()[srcIdx * elemSize],
+                lengthVal * elemSize);
+      }
+      return Flow();
+    }
     std::vector<Literal> copied;
     copied.resize(lengthVal);
     for (size_t i = 0; i < lengthVal; i++) {
-      copied[i] = srcData->values[srcVal + i];
+      copied[i] = srcVal.getElement(srcIdx + i);
     }
     for (size_t i = 0; i < lengthVal; i++) {
-      destData->values[destVal + i] = copied[i];
+      destVal.setElement(destIdx + i, copied[i]);
     }
     return Flow();
   }
@@ -2613,24 +2617,21 @@
     VISIT(index, curr->index)
     VISIT(value, curr->value)
     VISIT(size, curr->size)
-    auto data = ref.getSingleValue().getGCData();
-    if (!data) {
+    auto refVal = ref.getSingleValue();
+    if (refVal.isNull()) {
       trap("null ref");
     }
     size_t indexVal = index.getSingleValue().getUnsigned();
     Literal fillVal = value.getSingleValue();
     size_t sizeVal = size.getSingleValue().getUnsigned();
 
-    auto field = curr->ref->type.getHeapType().getArray().element;
-    fillVal = truncateForPacking(fillVal, field);
-
-    size_t arraySize = data->values.size();
+    size_t arraySize = refVal.getNumElements();
     if (indexVal > arraySize || sizeVal > arraySize ||
         indexVal + sizeVal > arraySize || indexVal + sizeVal < indexVal) {
       trap("out of bounds array access in array.fill");
     }
     for (size_t i = 0; i < sizeVal; ++i) {
-      data->values[indexVal + i] = fillVal;
+      refVal.setElement(indexVal + i, fillVal);
     }
     return {};
   }
@@ -2640,17 +2641,17 @@
     VISIT(ref, curr->ref)
     VISIT(index, curr->index)
     VISIT(value, curr->value)
-    auto data = ref.getSingleValue().getGCData();
-    if (!data) {
+    auto refVal = ref.getSingleValue();
+    if (refVal.isNull()) {
       trap("null ref");
     }
     size_t indexVal = index.getSingleValue().getUnsigned();
-    if (indexVal >= data->values.size()) {
+    if (indexVal >= refVal.getNumElements()) {
       trap("array oob");
     }
-    auto& field = data->values[indexVal];
-    auto oldVal = field;
-    field = applyRMW(curr->op, oldVal, value.getSingleValue());
+    auto oldVal = refVal.getElement(indexVal);
+    refVal.setElement(indexVal,
+                      applyRMW(curr->op, oldVal, value.getSingleValue()));
     return oldVal;
   }
 
@@ -2659,18 +2660,17 @@
     VISIT(index, curr->index)
     VISIT(expected, curr->expected)
     VISIT(replacement, curr->replacement)
-    auto data = ref.getSingleValue().getGCData();
-    if (!data) {
+    auto refVal = ref.getSingleValue();
+    if (refVal.isNull()) {
       trap("null ref");
     }
     size_t indexVal = index.getSingleValue().getUnsigned();
-    if (indexVal >= data->values.size()) {
+    if (indexVal >= refVal.getNumElements()) {
       trap("array oob");
     }
-    auto& field = data->values[indexVal];
-    auto oldVal = field;
-    if (field == expected.getSingleValue()) {
-      field = replacement.getSingleValue();
+    auto oldVal = refVal.getElement(indexVal);
+    if (oldVal == expected.getSingleValue()) {
+      refVal.setElement(indexVal, replacement.getSingleValue());
     }
     return oldVal;
   }
@@ -2696,22 +2696,21 @@
       case StringNewWTF16Array: {
         VISIT(start, curr->start)
         VISIT(end, curr->end)
-        auto ptrData = ptr.getSingleValue().getGCData();
-        if (!ptrData) {
+        auto ptrVal = ptr.getSingleValue();
+        if (ptrVal.isNull()) {
           trap("null ref");
         }
-        const auto& ptrDataValues = ptrData->values;
+        size_t arrayLen = ptrVal.getNumElements();
         size_t startVal = start.getSingleValue().getUnsigned();
         size_t endVal = end.getSingleValue().getUnsigned();
-        if (startVal > ptrDataValues.size() || endVal > ptrDataValues.size() ||
-            endVal < startVal) {
+        if (startVal > arrayLen || endVal > arrayLen || endVal < startVal) {
           trap("array oob");
         }
         Literals contents;
         if (endVal > startVal) {
           contents.reserve(endVal - startVal);
           for (size_t i = startVal; i < endVal; i++) {
-            contents.push_back(ptrDataValues[i]);
+            contents.push_back(ptrVal.getElement(i));
           }
         }
         return makeGCData(std::move(contents), curr->type);
@@ -2748,7 +2747,7 @@
       trap("null ref");
     }
 
-    return Literal(int32_t(data->values.size()));
+    return Literal(int32_t(data->getLiterals().size()));
   }
   Flow visitStringConcat(StringConcat* curr) {
     VISIT(flow, curr->left)
@@ -2761,17 +2760,19 @@
       trap("null ref");
     }
 
-    auto totalSize = leftData->values.size() + rightData->values.size();
+    auto totalSize =
+      leftData->getLiterals().size() + rightData->getLiterals().size();
     if (totalSize >= DataLimit) {
       hostLimit("allocation failure");
     }
 
     Literals contents;
-    contents.reserve(leftData->values.size() + rightData->values.size());
-    for (Literal& l : leftData->values) {
+    contents.reserve(leftData->getLiterals().size() +
+                     rightData->getLiterals().size());
+    for (Literal& l : leftData->getLiterals()) {
       contents.push_back(l);
     }
-    for (Literal& l : rightData->values) {
+    for (Literal& l : rightData->getLiterals()) {
       contents.push_back(l);
     }
 
@@ -2788,24 +2789,24 @@
     VISIT(start, curr->start)
 
     auto strData = str.getSingleValue().getGCData();
-    auto arrayData = array.getSingleValue().getGCData();
-    if (!strData || !arrayData) {
+    auto arrayVal = array.getSingleValue();
+    if (!strData || arrayVal.isNull()) {
       trap("null ref");
     }
     auto startVal = start.getSingleValue().getUnsigned();
-    auto& strValues = strData->values;
-    auto& arrayValues = arrayData->values;
+    auto& strValues = strData->getLiterals();
+    size_t arrayLen = arrayVal.getNumElements();
     size_t end;
     if (std::ckd_add<size_t>(&end, startVal, strValues.size()) ||
-        end > arrayValues.size()) {
+        end > arrayLen) {
       trap("oob");
     }
 
     for (Index i = 0; i < strValues.size(); i++) {
-      arrayValues[startVal + i] = strValues[i];
+      arrayVal.setElement(startVal + i, strValues[i]);
     }
 
-    return Literal(int32_t(strData->values.size()));
+    return Literal(int32_t(strValues.size()));
   }
   Flow visitStringEq(StringEq* curr) {
     VISIT(flow, curr->left)
@@ -2818,17 +2819,17 @@
     switch (curr->op) {
       case StringEqEqual: {
         // They are equal if both are null, or both are non-null and equal.
-        result =
-          (!leftData && !rightData) ||
-          (leftData && rightData && leftData->values == rightData->values);
+        result = (!leftData && !rightData) ||
+                 (leftData && rightData &&
+                  leftData->getLiterals() == rightData->getLiterals());
         break;
       }
       case StringEqCompare: {
         if (!leftData || !rightData) {
           trap("null ref");
         }
-        auto& leftValues = leftData->values;
-        auto& rightValues = rightData->values;
+        auto& leftValues = leftData->getLiterals();
+        auto& rightValues = rightData->getLiterals();
         Index i = 0;
         while (1) {
           if (i == leftValues.size() && i == rightValues.size()) {
@@ -2878,7 +2879,7 @@
     if (!data) {
       trap("null ref");
     }
-    auto& values = data->values;
+    auto& values = data->getLiterals();
     Index i = pos.getSingleValue().geti32();
     if (i >= values.size()) {
       trap("string oob");
@@ -2895,7 +2896,7 @@
     if (!refData) {
       trap("null ref");
     }
-    auto& refValues = refData->values;
+    auto& refValues = refData->getLiterals();
     auto startVal = start.getSingleValue().getUnsigned();
     auto endVal = end.getSingleValue().getUnsigned();
     endVal = std::min<size_t>(endVal, refValues.size());
@@ -2955,22 +2956,6 @@
     }
     return value;
   }
-
-  Literal makeFromMemory(void* p, Field field) {
-    switch (field.packedType) {
-      case Field::NotPacked:
-        return Literal::makeFromMemory(p, field.type);
-      case Field::i8: {
-        return truncateForPacking(Literal(int32_t(Bits::readLE<int8_t>(p))),
-                                  field);
-      }
-      case Field::i16: {
-        return truncateForPacking(Literal(int32_t(Bits::readLE<int16_t>(p))),
-                                  field);
-      }
-    }
-    WASM_UNREACHABLE("unexpected type");
-  }
 };
 
 // Execute a suspected constant expression (precompute and C-API).
@@ -4742,7 +4727,6 @@
 
     auto heapType = curr->type.getHeapType();
     const auto& element = heapType.getArray().element;
-    Literals contents;
 
     const auto& seg = *wasm.getDataSegment(curr->segment);
     auto elemBytes = element.getByteSize();
@@ -4757,10 +4741,9 @@
     if (droppedDataSegments.contains(curr->segment) && end > 0) {
       trap("dropped segment access in array.new_data");
     }
-    contents.reserve(size);
-    for (Index i = offset; i < end; i += elemBytes) {
-      auto addr = (void*)&seg.data[i];
-      contents.push_back(this->makeFromMemory(addr, element));
+    std::vector<uint8_t> contents(size * elemBytes);
+    if (size > 0) {
+      memcpy(contents.data(), &seg.data[offset], size * elemBytes);
     }
 
 #pragma GCC diagnostic pop
@@ -4796,15 +4779,15 @@
     VISIT(index, curr->index)
     VISIT(offset, curr->offset)
     VISIT(size, curr->size)
-    auto data = ref.getSingleValue().getGCData();
-    if (!data) {
+    auto refVal = ref.getSingleValue();
+    if (refVal.isNull()) {
       trap("null ref");
     }
     size_t indexVal = index.getSingleValue().getUnsigned();
     size_t offsetVal = offset.getSingleValue().getUnsigned();
     size_t sizeVal = size.getSingleValue().getUnsigned();
 
-    size_t arraySize = data->values.size();
+    size_t arraySize = refVal.getNumElements();
     if ((uint64_t)indexVal + sizeVal > arraySize) {
       trap("out of bounds array access in array.init");
     }
@@ -4822,9 +4805,11 @@
         droppedDataSegments.contains(curr->segment)) {
       trap("out of bounds segment access in array.init_data");
     }
-    for (size_t i = 0; i < sizeVal; i++) {
-      void* addr = (void*)&seg->data[offsetVal + i * elemSize];
-      data->values[indexVal + i] = this->makeFromMemory(addr, elem);
+    assert(refVal.isRawBytes());
+    if (sizeVal > 0) {
+      memcpy(&refVal.getRawBytes()[indexVal * elemSize],
+             &seg->data[offsetVal],
+             sizeVal * elemSize);
     }
     return {};
   }
@@ -4833,15 +4818,15 @@
     VISIT(index, curr->index)
     VISIT(offset, curr->offset)
     VISIT(size, curr->size)
-    auto data = ref.getSingleValue().getGCData();
-    if (!data) {
+    auto refVal = ref.getSingleValue();
+    if (refVal.isNull()) {
       trap("null ref");
     }
     size_t indexVal = index.getSingleValue().getUnsigned();
     size_t offsetVal = offset.getSingleValue().getUnsigned();
     size_t sizeVal = size.getSingleValue().getUnsigned();
 
-    size_t arraySize = data->values.size();
+    size_t arraySize = refVal.getNumElements();
     if ((uint64_t)indexVal + sizeVal > arraySize) {
       trap("out of bounds array access in array.init");
     }
@@ -4861,7 +4846,8 @@
       // of references in the table! ArrayNew suffers the same problem.
       // Fixing it will require changing how we represent segments, at least
       // in the interpreter.
-      data->values[indexVal + i] = self()->visit(seg->data[i]).getSingleValue();
+      refVal.setElement(indexVal + i,
+                        self()->visit(seg->data[i]).getSingleValue());
     }
     return {};
   }
diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp
index a037fef..272cc5a 100644
--- a/src/wasm/literal.cpp
+++ b/src/wasm/literal.cpp
@@ -314,7 +314,7 @@
   return makeFromInt32(-1, type);
 }
 
-Literal Literal::makeFromMemory(void* p, Type type) {
+Literal Literal::makeFromMemory(const void* p, Type type) {
   assert(type.isNumber());
   switch (type.getBasic()) {
     case Type::i32: {
@@ -499,7 +499,7 @@
       return *funcData == *other.funcData;
     }
     if (type.isString()) {
-      return gcData->values == other.gcData->values;
+      return gcData->getLiterals() == other.gcData->getLiterals();
     }
     if (type.isData()) {
       return gcData == other.gcData;
@@ -775,8 +775,8 @@
           break;
         case HeapType::any: {
           auto data = literal.getGCData();
-          assert(data->values.size() == 1);
-          o << "internalized " << literal.getGCData()->values[0];
+          assert(data->getLiterals().size() == 1);
+          o << "internalized " << data->getLiterals()[0];
           break;
         }
         case HeapType::ext: {
@@ -806,7 +806,7 @@
             o << "string(";
             // Convert WTF-16 literals to WTF-16 string.
             std::stringstream wtf16;
-            for (auto c : data->values) {
+            for (auto c : data->getLiterals()) {
               auto u = c.getInteger();
               assert(u < 0x10000);
               wtf16 << uint8_t(u & 0xFF);
@@ -844,9 +844,15 @@
       assert(literal.isData());
       auto data = literal.getGCData();
       assert(data);
-      o << "[ref " << literal.type.getHeapType() << ' ' << data->values;
+      o << "[ref " << literal.type.getHeapType() << ' ';
+      for (size_t i = 0; i < literal.getNumElements(); i++) {
+        if (i > 0) {
+          o << ' ';
+        }
+        o << literal.getElement(i);
+      }
       if (!data->desc.isNull()) {
-        if (!data->values.empty()) {
+        if (literal.getNumElements() > 0) {
           o << ", ";
         }
         o << "desc=" << data->desc;
@@ -3102,8 +3108,8 @@
   }
   if (heapType.isMaybeShared(HeapType::any)) {
     // This is an internalized externref or string; just unwrap it.
-    assert(gcData->values.size() == 1);
-    return gcData->values[0];
+    assert(gcData->getLiterals().size() == 1);
+    return gcData->getLiterals()[0];
   }
   // This is an internal reference. Wrap it.
   auto ext = HeapTypes::ext.getBasic(heapType.getShared());
@@ -3124,8 +3130,8 @@
     return Literal(std::make_shared<GCData>(Literals{*this}), any);
   }
   // This is an externalized internal reference; just unwrap it.
-  assert(gcData->values.size() == 1);
-  return gcData->values[0];
+  assert(gcData->getLiterals().size() == 1);
+  return gcData->getLiterals()[0];
 }
 
 Literal Literal::unwrap() const {
@@ -3148,7 +3154,7 @@
   assert(type.isRef());
   if (auto desc = type.getHeapType().getDescriptorType();
       desc && JSUtils::hasPossibleJSPrototypeField(*desc)) {
-    auto proto = gcData->desc.getGCData()->values[0].unwrap();
+    auto proto = gcData->desc.getGCData()->getLiterals()[0].unwrap();
     // Strings and numbers are not valid prototypes, so they appear as null.
     // Externref nulls are also converted to nullref.
     auto protoType = proto.type.getHeapType();
@@ -3161,4 +3167,79 @@
   return Literal::makeNull(HeapType::none);
 }
 
+size_t Literal::getNumElements() const {
+  assert(isData());
+  if (gcData->isRawBytes()) {
+    auto field = type.getHeapType().getArray().element;
+    return gcData->getRawBytes().size() / field.getByteSize();
+  }
+  return gcData->getLiterals().size();
+}
+
+namespace {
+
+void writeField(void* dest, const Field& field, Literal value) {
+  if (field.isPacked()) {
+    assert(field.type == Type::i32);
+    int32_t c = value.geti32();
+    if (field.packedType == Field::i8) {
+      Bits::writeLE<int8_t>(static_cast<int8_t>(c), dest);
+    } else if (field.packedType == Field::i16) {
+      Bits::writeLE<int16_t>(static_cast<int16_t>(c), dest);
+    } else {
+      WASM_UNREACHABLE("invalid packed type");
+    }
+    return;
+  }
+
+  uint8_t buf[16];
+  assert(field.getByteSize() <= sizeof(buf));
+  value.getBits(buf);
+  memcpy(dest, buf, field.getByteSize());
+}
+
+Literal readField(const void* src, const Field& field, bool signed_) {
+  if (field.isPacked()) {
+    assert(field.type == Type::i32);
+    if (field.packedType == Field::i8) {
+      int8_t val = Bits::readLE<int8_t>(src);
+      return Literal(signed_ ? int32_t(val)
+                             : int32_t(static_cast<uint8_t>(val)));
+    } else if (field.packedType == Field::i16) {
+      int16_t val = Bits::readLE<int16_t>(src);
+      return Literal(signed_ ? int32_t(val)
+                             : int32_t(static_cast<uint16_t>(val)));
+    } else {
+      WASM_UNREACHABLE("invalid packed type");
+    }
+  }
+
+  return Literal::makeFromMemory(src, field.type);
+}
+
+} // anonymous namespace
+
+Literal Literal::getElement(size_t index, bool signed_) const {
+  assert(isData());
+  if (gcData->isRawBytes()) {
+    auto field = type.getHeapType().getArray().element;
+    size_t elemSize = field.getByteSize();
+    assert((index + 1) * elemSize <= gcData->getRawBytes().size());
+    return readField(&gcData->getRawBytes()[index * elemSize], field, signed_);
+  }
+  return gcData->getLiterals()[index];
+}
+
+void Literal::setElement(size_t index, Literal value) {
+  assert(isData());
+  if (gcData->isRawBytes()) {
+    auto field = type.getHeapType().getArray().element;
+    size_t elemSize = field.getByteSize();
+    assert((index + 1) * elemSize <= gcData->getRawBytes().size());
+    writeField(&gcData->getRawBytes()[index * elemSize], field, value);
+    return;
+  }
+  gcData->getLiterals()[index] = value;
+}
+
 } // namespace wasm