Skip to content

Commit

Permalink
MemoryPacking: Handle non-empty trapping segments (WebAssembly#6261)
Browse files Browse the repository at this point in the history
Followup to WebAssembly#6243 which handled empty ones.
  • Loading branch information
kripken authored and radekdoulik committed Jul 12, 2024
1 parent ead8381 commit b78f620
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 9 deletions.
77 changes: 68 additions & 9 deletions src/passes/MemoryPacking.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "ir/utils.h"
#include "pass.h"
#include "support/space.h"
#include "support/stdckdint.h"
#include "wasm-binary.h"
#include "wasm-builder.h"
#include "wasm.h"
Expand All @@ -46,6 +47,7 @@ namespace {
// will be used instead of memory.init for this range.
struct Range {
bool isZero;
// The range [start, end) - that is, start is included while end is not.
size_t start;
size_t end;
};
Expand Down Expand Up @@ -109,7 +111,8 @@ struct MemoryPacking : public Pass {
ReferrersMap& referrers);
bool canSplit(const std::unique_ptr<DataSegment>& segment,
const Referrers& referrers);
void calculateRanges(const std::unique_ptr<DataSegment>& segment,
void calculateRanges(Module* module,
const std::unique_ptr<DataSegment>& segment,
const Referrers& referrers,
std::vector<Range>& ranges);
void createSplitSegments(Builder& builder,
Expand Down Expand Up @@ -162,7 +165,7 @@ void MemoryPacking::run(Module* module) {
std::vector<Range> ranges;

if (canSplit(segment, currReferrers)) {
calculateRanges(segment, currReferrers, ranges);
calculateRanges(module, segment, currReferrers, ranges);
} else {
// A single range covers the entire segment. Set isZero to false so the
// original memory.init will be used even if segment is all zeroes.
Expand Down Expand Up @@ -295,14 +298,33 @@ bool MemoryPacking::canSplit(const std::unique_ptr<DataSegment>& segment,
return segment->isPassive || segment->offset->is<Const>();
}

void MemoryPacking::calculateRanges(const std::unique_ptr<DataSegment>& segment,
void MemoryPacking::calculateRanges(Module* module,
const std::unique_ptr<DataSegment>& segment,
const Referrers& referrers,
std::vector<Range>& ranges) {
auto& data = segment->data;
if (data.size() == 0) {
return;
}

// A segment that might trap during startup must be handled carefully, as we
// do not want to remove that trap (unless we are allowed to by TNH).
auto preserveTrap = !getPassOptions().trapsNeverHappen && segment->offset;
if (preserveTrap) {
// Check if we can rule out a trap by it being in bounds.
if (auto* c = segment->offset->dynCast<Const>()) {
auto* memory = module->getMemory(segment->memory);
auto memorySize = memory->initial * Memory::kPageSize;
Index start = c->value.getUnsigned();
Index size = segment->data.size();
Index end;
if (!std::ckd_add(&end, start, size) && end <= memorySize) {
// This is in bounds.
preserveTrap = false;
}
}
}

// Calculate initial zero and nonzero ranges
size_t start = 0;
while (start < data.size()) {
Expand Down Expand Up @@ -346,7 +368,10 @@ void MemoryPacking::calculateRanges(const std::unique_ptr<DataSegment>& segment,
}
}

// Merge edge zeroes if they are not worth splitting
// Merge edge zeroes if they are not worth splitting. Note that we must not
// have a trap we must preserve here (if we did, we'd need to handle that in
// the code below).
assert(!preserveTrap);
if (ranges.size() >= 2) {
auto last = ranges.end() - 1;
auto penultimate = ranges.end() - 2;
Expand All @@ -364,12 +389,12 @@ void MemoryPacking::calculateRanges(const std::unique_ptr<DataSegment>& segment,
}
}
} else {
// Legacy ballpark overhead of active segment with offset
// Legacy ballpark overhead of active segment with offset.
// TODO: Tune this
threshold = 8;
}

// Merge ranges across small spans of zeroes
// Merge ranges across small spans of zeroes.
std::vector<Range> mergedRanges = {ranges.front()};
size_t i;
for (i = 1; i < ranges.size() - 1; ++i) {
Expand All @@ -383,10 +408,28 @@ void MemoryPacking::calculateRanges(const std::unique_ptr<DataSegment>& segment,
mergedRanges.push_back(*curr);
}
}
// Add the final range if it hasn't already been merged in
// Add the final range if it hasn't already been merged in.
if (i < ranges.size()) {
mergedRanges.push_back(ranges.back());
}
// If we need to preserve a trap then we must keep the topmost byte of the
// segment, which is enough to cause a trap if we do in fact trap.
if (preserveTrap) {
// Check if the last byte is in a zero range. Such a range will be dropped
// later, so add a non-zero range with that byte. (It is slightly odd to
// add a range with a zero marked as non-zero, but that is how we ensure it
// is preserved later in the output.)
auto& back = mergedRanges.back();
if (back.isZero) {
// Remove the last byte from |back|. Decrementing this prevents it from
// overlapping with the new segment we are about to add. Note that this
// might make |back| have size 0, but that is not a problem as it will be
// dropped later anyhow, since it contains zeroes.
back.end--;
auto lastByte = data.size() - 1;
mergedRanges.push_back({false, lastByte, lastByte + 1});
}
}
std::swap(ranges, mergedRanges);
}

Expand Down Expand Up @@ -551,6 +594,20 @@ void MemoryPacking::dropUnusedSegments(
module->updateDataSegmentsMap();
}

// Given the start of a segment and an offset into it, compute the sum of the
// two to get the absolute address the data should be at. This takes into
// account overflows in that we saturate to UINT_MAX: we do not want an overflow
// to take us down into a small address; in the invalid case of an overflow we
// stay at the largest possible unsigned value, which will keep us trapping.
template<typename T>
Expression* addStartAndOffset(T start, T offset, Builder& builder) {
T total;
if (std::ckd_add(&total, start, offset)) {
total = std::numeric_limits<T>::max();
}
return builder.makeConst(T(total));
}

void MemoryPacking::createSplitSegments(
Builder& builder,
const DataSegment* segment,
Expand All @@ -568,10 +625,12 @@ void MemoryPacking::createSplitSegments(
if (!segment->isPassive) {
if (auto* c = segment->offset->dynCast<Const>()) {
if (c->value.type == Type::i32) {
offset = builder.makeConst(int32_t(c->value.geti32() + range.start));
offset = addStartAndOffset<uint32_t>(
range.start, c->value.geti32(), builder);
} else {
assert(c->value.type == Type::i64);
offset = builder.makeConst(int64_t(c->value.geti64() + range.start));
offset = addStartAndOffset<uint64_t>(
range.start, c->value.geti64(), builder);
}
} else {
assert(ranges.size() == 1);
Expand Down
85 changes: 85 additions & 0 deletions test/lit/passes/memory-packing_traps.wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.

;; RUN: foreach %s %t wasm-opt --memory-packing -all --zero-filled-memory -S -o - | filecheck %s
;; RUN: foreach %s %t wasm-opt --memory-packing -all --zero-filled-memory -tnh -S -o - | filecheck %s --check-prefix=TNH__

(module
;; We should not optimize out a segment that will trap, as that is an effect
;; we need to preserve (unless TrapsNeverHappen).
;; CHECK: (memory $memory 1 2)
;; TNH__: (memory $memory 1 2)
(memory $memory 1 2)
;; CHECK: (data $data (i32.const -1) "\00")
(data $data (i32.const -1) "\00")
)

(module
;; We should handle the possible overflow in adding the offset and size, and
;; see this might trap. To keep the segment trapping, we will emit a segment
;; with offset -1 of size 1 (which is the minimal thing we need for a trap).
;; CHECK: (memory $memory 1 2)
;; TNH__: (memory $memory 1 2)
(memory $memory 1 2)
;; CHECK: (data $data (i32.const -1) "\00")
(data $data (i32.const -2) "\00\00\00")
)

(module
;; This segment will almost trap, but not.
;; CHECK: (memory $memory 1 2)
;; TNH__: (memory $memory 1 2)
(memory $memory 1 2)
(data $data (i32.const 65535) "\00")
)

(module
;; This one is slightly larger, and will trap. We can at least shorten the
;; segment to only contain one byte, at the highest address the segment would
;; write to.
;; CHECK: (memory $memory 1 2)
;; TNH__: (memory $memory 1 2)
(memory $memory 1 2)
;; CHECK: (data $data (i32.const 65536) "\00")
(data $data (i32.const 65535) "\00\00")
)

(module
;; This one is slightly larger, but the offset is lower so it will not trap.
;; CHECK: (memory $memory 1 2)
;; TNH__: (memory $memory 1 2)
(memory $memory 1 2)
(data $data (i32.const 65534) "\00\00")
)

(module
;; This one's offset is just large enough to trap.
;; CHECK: (memory $memory 1 2)
;; TNH__: (memory $memory 1 2)
(memory $memory 1 2)
;; CHECK: (data $data (i32.const 65536) "\00")
(data $data (i32.const 65536) "\00")
)

(module
;; This offset is unknown, so assume the worst.
;; TODO: We could remove it in TNH mode

;; CHECK: (import "a" "b" (global $g i32))
;; TNH__: (import "a" "b" (global $g i32))
(import "a" "b" (global $g i32))
;; CHECK: (memory $memory 1 2)
;; TNH__: (memory $memory 1 2)
(memory $memory 1 2)
;; CHECK: (data $data (global.get $g) "\00")
;; TNH__: (data $data (global.get $g) "\00")
(data $data (global.get $g) "\00")
)

(module
;; Passive segments cannot trap during startup and are removable if they have
;; no uses, like here.
;; CHECK: (memory $memory 1 2)
;; TNH__: (memory $memory 1 2)
(memory $memory 1 2)
(data $data "\00\00\00")
)

0 comments on commit b78f620

Please sign in to comment.