Improve use of data section for RW data

8a64c0a3cca95edb4c6e508e9f6ab5b5cef2f5735b68a14b39c7b06901bb381b
Previously we weren't taking all opportunities to use BSS over RW
data. Now we check aggregates for zero-ness.
Alexis Sellier committed ago 1 parent 364f0a91
compiler/radiance.rad +1 -4
752 752
753 753
    return unix::writeFile(path, bytes);
754 754
}
755 755
756 756
/// Write a data section to a file at `basePath` + `ext`.
757 -
/// If the data is empty, no file is written.
757 +
/// Empty data truncates any stale sidecar left by an earlier build.
758 758
fn writeDataWithExt(
759 759
    data: *[u8],
760 760
    basePath: *[u8],
761 761
    ext: *[u8]
762 762
) throws (Error) {
763 -
    if data.len == 0 {
764 -
        return;
765 -
    }
766 763
    let mut path: [u8; MAX_PATH_LEN] = undefined;
767 764
    let mut pos: u32 = 0;
768 765
769 766
    set pos += try! mem::copy(&mut path[pos..], basePath);
770 767
    set pos += try! mem::copy(&mut path[pos..], ext);
lib/std/lang/gen/data.rad +8 -8
30 30
    /// Fallback linear array for edge cases.
31 31
    syms: *[DataSym],
32 32
}
33 33
34 34
/// Lay out data symbols for a single section.
35 -
/// Initialized data is placed first, then uninitialized, so that only
36 -
/// initialized data needs to be written to the output file.
35 +
/// Data with sidecar image bytes is placed first, then zero-initialized data,
36 +
/// so that only meaningful bytes need to be written to the output file.
37 37
/// Returns the updated offset past all placed symbols.
38 38
export fn layoutSection(
39 39
    items: *[il::Data],
40 40
    syms: *mut [DataSym],
41 41
    count: *mut u32,
54 54
    startOffset: u32,
55 55
    readOnly: bool
56 56
) -> u32 {
57 57
    let mut offset: u32 = startOffset;
58 58
59 -
    // Initialized data first.
59 +
    // Data requiring sidecar image bytes first.
60 60
    for i in 0..items.len {
61 61
        let data = &items[i];
62 -
        if data.readOnly == readOnly and not data.isUndefined {
62 +
        if data.readOnly == readOnly and not data.isZeroInit {
63 63
            set offset = mem::alignUp(offset, data.alignment);
64 64
            set syms[*count] = DataSym { name: data.name, addr: base + offset };
65 65
            set *count += 1;
66 66
            set offset += data.size;
67 67
        }
68 68
    }
69 -
    // Uninitialized data after.
69 +
    // Zero-initialized data after.
70 70
    for i in 0..items.len {
71 71
        let data = &items[i];
72 -
        if data.readOnly == readOnly and data.isUndefined {
72 +
        if data.readOnly == readOnly and data.isZeroInit {
73 73
            set offset = mem::alignUp(offset, data.alignment);
74 74
            set syms[*count] = DataSym { name: data.name, addr: base + offset };
75 75
            set *count += 1;
76 76
            set offset += data.size;
77 77
        }
78 78
    }
79 79
    return offset;
80 80
}
81 81
82 82
/// Emit data bytes for a single section (read-only or read-write) into `buf`.
83 -
/// Iterates initialized data in the IL program, serializing each data item.
83 +
/// Iterates data requiring sidecar image bytes, serializing each data item.
84 84
/// Returns the total number of bytes written.
85 85
export fn emitSection(
86 86
    items: *[il::Data],
87 87
    dataSymMap: *DataSymMap,
88 88
    fnLabels: *labels::Labels,
105 105
) -> u32 {
106 106
    let mut offset: u32 = startOffset;
107 107
108 108
    for i in 0..items.len {
109 109
        let data = &items[i];
110 -
        if data.readOnly == readOnly and not data.isUndefined {
110 +
        if data.readOnly == readOnly and not data.isZeroInit {
111 111
            set offset = mem::alignUp(offset, data.alignment);
112 112
            assert offset + data.size <= buf.len, "emitSectionAtOffset: buffer overflow";
113 113
            for j in 0..data.values.len {
114 114
                let v = &data.values[j];
115 115
                for _ in 0..v.count {
lib/std/lang/il.rad +3 -3
376 376
    alignment: u32,
377 377
    /// Whether this is read-only data.
378 378
    /// Typically, `constant` declaration are read-only, while `static`
379 379
    /// declarations are not.
380 380
    readOnly: bool,
381 -
    /// Whether the data is entirely undefined.
382 -
    /// Undefined data doesn't need to be written since memory is zero-initialized.
383 -
    isUndefined: bool,
381 +
    /// Whether writable data can be represented by zero-filled memory.
382 +
    /// Zero-initialized data doesn't need to be written to the sidecar image.
383 +
    isZeroInit: bool,
384 384
    /// Initializer values.
385 385
    values: *[DataValue],
386 386
}
387 387
388 388
/// An IL program (compilation unit).
lib/std/lang/lower.rad +52 -12
379 379
380 380
/// Builder for accumulating data values during constant lowering.
381 381
record DataValueBuilder {
382 382
    allocator: alloc::Allocator,
383 383
    values: *mut [il::DataValue],
384 -
    /// Whether all values pushed are undefined.
385 -
    allUndef: bool,
384 +
    /// Whether all pushed values can be represented by zero-filled memory.
385 +
    zeroInit: bool,
386 386
}
387 387
388 388
/// Result of lowering constant data.
389 389
record ConstDataResult {
390 390
    values: *[il::DataValue],
391 -
    isUndefined: bool,
391 +
    zeroInit: bool,
392 392
}
393 393
394 394
/// Create a new builder.
395 395
fn dataBuilder(allocator: alloc::Allocator) -> DataValueBuilder {
396 -
    return DataValueBuilder { allocator, values: &mut [], allUndef: true };
396 +
    return DataValueBuilder { allocator, values: &mut [], zeroInit: true };
397 +
}
398 +
399 +
/// Return whether a data item can be omitted from a zero-filled image.
400 +
fn dataIsZero(item: il::DataItem) -> bool {
401 +
    match item {
402 +
        case il::DataItem::Undef => return true,
403 +
        case il::DataItem::Val { val, .. } => return val == 0,
404 +
        else => return false,
405 +
    }
406 +
}
407 +
408 +
/// Return whether all data values can be omitted from a zero-filled image.
409 +
fn dataValuesAreZeroInit(values: *[il::DataValue]) -> bool {
410 +
    for value in values {
411 +
        if value.count > 0 and not dataIsZero(value.item) {
412 +
            return false;
413 +
        }
414 +
    }
415 +
    return true;
397 416
}
398 417
399 418
/// Append a data value to the builder.
400 419
fn dataBuilderPush(b: *mut DataValueBuilder, value: il::DataValue) {
401 420
    b.values.append(value, b.allocator);
402 421
403 -
    if value.item <> il::DataItem::Undef {
404 -
        set b.allUndef = false;
422 +
    if value.count > 0 and not dataIsZero(value.item) {
423 +
        set b.zeroInit = false;
405 424
    }
406 425
}
407 426
408 427
/// Return the accumulated values.
409 428
fn dataBuilderFinish(b: *DataValueBuilder) -> ConstDataResult {
410 429
    return ConstDataResult {
411 430
        values: &b.values[..],
412 -
        isUndefined: b.allUndef,
431 +
        zeroInit: b.zeroInit,
413 432
    };
414 433
}
415 434
416 435
///////////////////////////
417 436
// SSA Variable Tracking //
1116 1135
    self.data.append(il::Data {
1117 1136
        name: vName,
1118 1137
        size: traitInfo.methods.len as u32 * resolver::PTR_SIZE,
1119 1138
        alignment: resolver::PTR_SIZE,
1120 1139
        readOnly: true,
1121 -
        isUndefined: false,
1140 +
        isZeroInit: false,
1122 1141
        values: &values[..traitInfo.methods.len as u32],
1123 1142
    }, self.allocator);
1124 1143
}
1125 1144
1126 1145
/// Lower a method node into an IL function with the given qualified name.
1324 1343
    self.data.append(il::Data {
1325 1344
        name: qualName,
1326 1345
        size: layout.size,
1327 1346
        alignment: layout.alignment,
1328 1347
        readOnly,
1329 -
        isUndefined: result.isUndefined,
1348 +
        isZeroInit: not readOnly and result.zeroInit,
1330 1349
        values: result.values,
1331 1350
    }, self.allocator);
1332 1351
}
1333 1352
1334 1353
/// Emit the in-memory representation of a slice header: `{ ptr, len, cap }`.
1717 1736
    readOnly: bool,
1718 1737
    values: *[il::DataValue],
1719 1738
    dataPrefix: *[u8]
1720 1739
) -> *[u8] throws (LowerError) {
1721 1740
    let name = try nextDeclDataName(self, dataPrefix, self.data.len, "literal");
1722 -
    self.data.append(il::Data { name, size, alignment, readOnly, isUndefined: false, values }, self.allocator);
1741 +
    self.data.append(il::Data {
1742 +
        name,
1743 +
        size,
1744 +
        alignment,
1745 +
        readOnly,
1746 +
        isZeroInit: not readOnly and dataValuesAreZeroInit(values),
1747 +
        values,
1748 +
    }, self.allocator);
1723 1749
1724 1750
    return name;
1725 1751
}
1726 1752
1727 1753
/// Find or create read-only string data and return its symbol name.
1838 1864
    if readOnly {
1839 1865
        if let found = findConstData(self.low, values, alignment) {
1840 1866
            set dataName = found;
1841 1867
        } else {
1842 1868
            set dataName = try nextDataName(self);
1843 -
            self.low.data.append(il::Data { name: dataName, size, alignment, readOnly, isUndefined: false, values }, self.low.allocator);
1869 +
            self.low.data.append(il::Data {
1870 +
                name: dataName,
1871 +
                size,
1872 +
                alignment,
1873 +
                readOnly,
1874 +
                isZeroInit: false,
1875 +
                values,
1876 +
            }, self.low.allocator);
1844 1877
        }
1845 1878
    } else {
1846 1879
        set dataName = try nextDataName(self);
1847 -
        self.low.data.append(il::Data { name: dataName, size, alignment, readOnly, isUndefined: false, values }, self.low.allocator);
1880 +
        self.low.data.append(il::Data {
1881 +
            name: dataName,
1882 +
            size,
1883 +
            alignment,
1884 +
            readOnly,
1885 +
            isZeroInit: dataValuesAreZeroInit(values),
1886 +
            values,
1887 +
        }, self.low.allocator);
1848 1888
    }
1849 1889
1850 1890
    // Get data address.
1851 1891
    let ptrReg = nextReg(self);
1852 1892
    emit(self, il::Instr::Copy { dst: ptrReg, val: il::Val::DataSym(dataName) });
test/run +28 -0
8 8
# For each test:
9 9
#   - If a `.ril` file exists alongside it, the IL output is checked
10 10
#     against it via the runner binary.
11 11
#   - If `//! returns: N` appears in the file, the test is compiled to
12 12
#     a binary and executed; the exit code must match N.
13 +
#   - If `//! rw-data-size: N` appears in the file, the emitted `.rw.data`
14 +
#     sidecar size must match N bytes. Missing sidecars count as zero bytes.
13 15
14 16
RUNNER="test/runner.rv64"
15 17
TEST_DIR="test/tests"
16 18
EMU="${RAD_EMULATOR:-emulator} -stack-size=1024 -run"
17 19
EMU_RUN="${RAD_EMULATOR:-emulator} -no-jit -run"
47 49
    *) base="$test" ;;
48 50
  esac
49 51
50 52
  ril="${base}.ril"
51 53
  bin="${base}.rv64"
54 +
  rw_data="${bin}.rw.data"
52 55
53 56
  # IL check: run the runner if a .ril file exists.
54 57
  if [ -f "$ril" ]; then
55 58
    if $EMU "$RUNNER" -- "$test"; then
56 59
      passed=$((passed + 1))
79 82
    else
80 83
      echo "FAILED (expected: $returns, got: $ret)"
81 84
      failed=$((failed + 1))
82 85
    fi
83 86
  fi
87 +
88 +
  rw_data_size=$(grep -m1 '^//! rw-data-size:' "$test" | sed 's/^\/\/! rw-data-size: *//')
89 +
  if [ -n "$rw_data_size" ]; then
90 +
    echo -n "test $test rw.data ... "
91 +
92 +
    if [ ! -f "$bin" ]; then
93 +
      echo "FAILED (binary not found: $bin)"
94 +
      failed=$((failed + 1))
95 +
      continue
96 +
    fi
97 +
98 +
    if [ -f "$rw_data" ]; then
99 +
      actual_rw_data_size=$(wc -c < "$rw_data" | tr -d ' ')
100 +
    else
101 +
      actual_rw_data_size=0
102 +
    fi
103 +
104 +
    if [ "$actual_rw_data_size" -eq "$rw_data_size" ]; then
105 +
      echo "ok"
106 +
      passed=$((passed + 1))
107 +
    else
108 +
      echo "FAILED (expected: $rw_data_size, got: $actual_rw_data_size)"
109 +
      failed=$((failed + 1))
110 +
    fi
111 +
  fi
84 112
done
85 113
echo
86 114
87 115
if [ "$failed" -eq 0 ]; then
88 116
  echo "test result: ok. $passed passed; $failed failed"
test/tests/static.zero.bss.rad added +45 -0
1 +
//! returns: 0
2 +
//! rw-data-size: 0
3 +
4 +
/// Static pool used to verify that partially initialized zero data is BSS.
5 +
record Pool {
6 +
    /// Undefined slice table.
7 +
    table: [*[u8]; 32],
8 +
    /// Explicitly initialized count.
9 +
    count: u32,
10 +
}
11 +
12 +
/// Padded record used to verify that zero fields plus undefined padding are BSS.
13 +
record Padded {
14 +
    /// Leading byte.
15 +
    tag: u8,
16 +
    /// Field that forces padding after `tag`.
17 +
    value: u32,
18 +
}
19 +
20 +
/// Partially initialized aggregate where all concrete bytes are zero.
21 +
static POOL: Pool = Pool { table: undefined, count: 0 };
22 +
23 +
/// Repeated scalar zeros.
24 +
static FLAGS: [bool; 4] = [false; 4];
25 +
26 +
/// Explicit zero fields with undefined padding.
27 +
static PADDED: Padded = Padded { tag: 0, value: 0 };
28 +
29 +
@default fn main() -> i32 {
30 +
    assert POOL.count == 0;
31 +
    assert FLAGS[0] == false;
32 +
    assert FLAGS[3] == false;
33 +
    assert PADDED.tag == 0;
34 +
    assert PADDED.value == 0;
35 +
36 +
    set POOL.count = 3;
37 +
    set FLAGS[2] = true;
38 +
    set PADDED.value = 9;
39 +
40 +
    assert POOL.count == 3;
41 +
    assert FLAGS[2] == true;
42 +
    assert PADDED.value == 9;
43 +
44 +
    return 0;
45 +
}