mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
x86: SSE under MXCSR's rounding control is a per-program opt-in, tested once a run
SSE under MXCSR is now asked for by the program that needs it: gex_ir::set_mxcsr_exact (the page's ?mxcsr=1, gex_set_mxcsr_exact; GEX_MXCSR=1 natively), before anything is translated. Off, the lifter makes no Expr::Mx and tier-1 and the function JIT import no `mx`: every tier's output is what it was before SSE honoured MXCSR (tier-1's module and the function tier's C byte for byte), and the keys are as they were. On, the unit key names it (`:mxcsr`), as do tier-1's knobs and the page's translation cache, so units with it and without it never mix. It is the rounding control only: FTZ and DAZ are honoured in no tier, as before, and the exact import is given the rounding control alone. A program that runs with FTZ or DAZ set (much audio code) keeps the fast path. The function tier tests the rounding control once for a run of up to 32 operations in a block and emits the run twice: the operations' own C, taken when it is to nearest, and their exact calls, out of line in runtime/mx.h (one call an operation, however many lanes). A block that writes MXCSR or makes a host call tests each operation. A test and an inline slow path per operation took clang over 65 minutes and past 15 GB on one function of 13,600 SSE operations; the runs take it 41 s and 287 MB, against 13 s and 194 MB without MXCSR. Tier-1 and the function JIT test each operation's rounding control and call the import. Tests: gex-lift-x86 sse_is_under_mxcsr_only_when_asked (no Mx, no MXCSR read without the flag; one per operation with it); gex-cgen sse_operations_under_mxcsr_test_once_a_block (without it, no MXCSR in the C; with it, one test a run, one out-of-line call an operation, at most twice the C, and clang takes it); tests/x86-programs fp-modes under GEX_MXCSR=1, its four rounding modes against the hardware on tier-1, the function JIT, the function tier's C and the page. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
This commit is contained in:
1 parent
eda608505f
commit
0d1f3888fa
20 files changed
+350
-205
No files matched your search
@@ -940,9 +940,10 @@ pub mod imports {
|
||||
/// `ctx::X87_TMP + 12`.
|
||||
pub const X87: &str = "x87";
|
||||
|
||||
/// An SSE operation under a non-default MXCSR (`gex_ir::Expr::Mx`'s
|
||||
/// slow path: a rounding control other than to nearest, FTZ or DAZ),
|
||||
/// computed exactly (`gex_f80::sse::op`). Pure, as [`X87`] is.
|
||||
/// An SSE operation under a rounding control other than to nearest
|
||||
/// (`gex_ir::Expr::Mx`'s slow path), computed exactly
|
||||
/// (`gex_f80::sse::op`). Pure, as [`X87`] is. Its `mxcsr` holds only
|
||||
/// the rounding control (`gex_ir::MX_SPECIAL`).
|
||||
/// Signature `(ctx: i32, code: i32, a: i64, b: i64, mxcsr: i32) -> i64`:
|
||||
/// `code` is `gex_f80::sse`'s, the operands and the result raw bits
|
||||
/// (a single's or an `i32`'s zero-extended).
|
||||
|
||||
@@ -1,42 +1,37 @@
|
||||
/* The function tier's slow path of Expr::Mx (an SSE operation under a
|
||||
* MXCSR with a rounding control other than to nearest, FTZ or DAZ): the
|
||||
* gex.mx import, which computes it exactly (gex_f80::sse::op), as tier-1's
|
||||
* `emit_mx` calls it -- once for a scalar, once per lane, lanes past `n`
|
||||
* zero. The fast path is the operation's own C: kept when none of those
|
||||
* bits is set, and under FTZ or DAZ alone when no float operand nor the
|
||||
* result is a denormal or the smallest normal (the gex_mx_tiny tests), as
|
||||
* neither flush then has anything to do.
|
||||
* MXCSR rounding control other than to nearest): the gex.mx import, which
|
||||
* computes it exactly (gex_f80::sse::op), as tier-1's `emit_mx` calls it --
|
||||
* once for a scalar, once per lane, lanes past `n` zero.
|
||||
*
|
||||
* Out of line (GEX_MX_SLOW), so an operation's slow path is one call in
|
||||
* the C, however many lanes. The emitter tests the rounding control once
|
||||
* for a run of operations and computes the run either way: as the
|
||||
* operations' own C or as these calls.
|
||||
*
|
||||
* `f`, a vector operation's shape: bits 0-3 the lanes `n`, bit 4 an 8-byte
|
||||
* operand lane, bit 5 an 8-byte result lane.
|
||||
*
|
||||
* Included from gex_abi.h, after the imports. */
|
||||
|
||||
/* A denormal or the smallest normal (|x| - 1 below the smallest normal). */
|
||||
GEX_INLINE int gex_mx_tiny32(uint32_t x) { return ((x & 0x7fffffffu) - 1) >> 23 == 0; }
|
||||
GEX_INLINE int gex_mx_tiny64(uint64_t x) { return ((x & 0x7fffffffffffffffull) - 1) >> 52 == 0; }
|
||||
/* Any lane of `v`, lanes `w` bytes wide. */
|
||||
GEX_INLINE int gex_mx_tiny_v(v128_t v, uint32_t w) {
|
||||
if (w == 4) {
|
||||
v128_t t = wasm_u32x4_shr(wasm_i32x4_sub(wasm_v128_and(v, wasm_i32x4_splat(0x7fffffff)), wasm_i32x4_splat(1)), 23);
|
||||
return wasm_v128_any_true(wasm_i32x4_eq(t, wasm_i32x4_splat(0)));
|
||||
}
|
||||
v128_t t = wasm_u64x2_shr(wasm_i64x2_sub(wasm_v128_and(v, wasm_i64x2_splat(0x7fffffffffffffffll)), wasm_i64x2_splat(1)), 52);
|
||||
return wasm_v128_any_true(wasm_i64x2_eq(t, wasm_i64x2_splat(0)));
|
||||
}
|
||||
#define GEX_MX_SLOW static __attribute__((noinline, unused))
|
||||
|
||||
GEX_INLINE uint64_t gex_mx_scalar(int ctx, int32_t code, uint64_t a, uint64_t b, uint32_t mx) {
|
||||
GEX_MX_SLOW uint64_t gex_mx_s(int ctx, int32_t code, uint64_t a, uint64_t b, uint32_t mx) {
|
||||
return (uint64_t)gex_mx(ctx, code, (int64_t)a, (int64_t)b, (int32_t)mx);
|
||||
}
|
||||
|
||||
/* Lane `k` (a constant) of `v`, `w` bytes wide, as the import's operand. */
|
||||
#define GEX_MX_IN(v, k, w) \
|
||||
((w) == 4 ? (uint64_t)(uint32_t)wasm_i32x4_extract_lane(v, k) : (uint64_t)wasm_i64x2_extract_lane(v, (k) & 1))
|
||||
/* Lane `k` (a constant) of `v`, 8 bytes wide when `w8`, as the import's operand. */
|
||||
#define GEX_MX_IN(v, k, w8) \
|
||||
(!(w8) ? (uint64_t)(uint32_t)wasm_i32x4_extract_lane(v, k) : (uint64_t)wasm_i64x2_extract_lane(v, (k) & 1))
|
||||
|
||||
GEX_INLINE v128_t gex_mx_lanes(int ctx, int32_t code, v128_t a, v128_t b, uint32_t n, uint32_t in_bytes,
|
||||
uint32_t out_bytes, uint32_t mx) {
|
||||
/* An absent `b` is all zero. */
|
||||
GEX_MX_SLOW v128_t gex_mx_v(int ctx, int32_t code, v128_t a, v128_t b, uint32_t f, uint32_t mx) {
|
||||
int in8 = (f & 16u) != 0, out8 = (f & 32u) != 0;
|
||||
uint32_t n = f & 15u;
|
||||
v128_t r = wasm_i64x2_const(0, 0);
|
||||
#define GEX_MX_LANE(k) \
|
||||
if ((k) < n) { \
|
||||
uint64_t x = gex_mx_scalar(ctx, code, GEX_MX_IN(a, k, in_bytes), GEX_MX_IN(b, k, in_bytes), mx); \
|
||||
r = out_bytes == 4 ? wasm_i32x4_replace_lane(r, k, (int32_t)x) : wasm_i64x2_replace_lane(r, (k) & 1, (int64_t)x); \
|
||||
#define GEX_MX_LANE(k) \
|
||||
if ((k) < n) { \
|
||||
uint64_t x = (uint64_t)gex_mx(ctx, code, (int64_t)GEX_MX_IN(a, k, in8), (int64_t)GEX_MX_IN(b, k, in8), (int32_t)mx); \
|
||||
r = !out8 ? wasm_i32x4_replace_lane(r, k, (int32_t)x) : wasm_i64x2_replace_lane(r, (k) & 1, (int64_t)x); \
|
||||
}
|
||||
GEX_MX_LANE(0)
|
||||
GEX_MX_LANE(1)
|
||||
|
||||
+109
-41
@@ -257,6 +257,26 @@ pub(crate) struct Emitter<'a> {
|
||||
/// (`GuestReg::CtxAddr`: the x87 stack, `EFLAGS_SYS`), whose accesses
|
||||
/// the null guard leaves out: the context is below it.
|
||||
value_ctx: Vec<bool>,
|
||||
/// How an `Expr::Mx` is lowered here ([`Emitter::stmts`]).
|
||||
mx_mode: MxMode,
|
||||
/// Values are declared before their definitions: a definition assigns.
|
||||
predeclared: bool,
|
||||
}
|
||||
|
||||
/// The most `Expr::Mx`s under one test of the rounding control
|
||||
/// ([`Emitter::stmts`]).
|
||||
const MX_RUN: usize = 32;
|
||||
|
||||
/// How [`Emitter::def`] lowers an `Expr::Mx`.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
enum MxMode {
|
||||
/// The operation's own C, then the exact call when the rounding control
|
||||
/// is not to nearest: one test per operation.
|
||||
Each,
|
||||
/// The operation's own C: the run's test found rounding to nearest.
|
||||
Own,
|
||||
/// The exact call: the run's test found another rounding.
|
||||
Exact,
|
||||
}
|
||||
|
||||
impl<'a> Emitter<'a> {
|
||||
@@ -280,6 +300,8 @@ impl<'a> Emitter<'a> {
|
||||
value_zext: vec![false; n],
|
||||
value_disp: vec![None; n],
|
||||
value_ctx: vec![false; n],
|
||||
mx_mode: MxMode::Each,
|
||||
predeclared: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,9 +344,7 @@ impl<'a> Emitter<'a> {
|
||||
if gex_abi::DISPATCH_SHIFT > gex_abi::INST_ALIGN_SHIFT {
|
||||
self.target_check();
|
||||
}
|
||||
for s in &block.stmts {
|
||||
self.stmt(s)?;
|
||||
}
|
||||
self.stmts(&block.stmts, |e, k| e.stmt(&block.stmts[k]))?;
|
||||
self.terminator()
|
||||
}
|
||||
|
||||
@@ -1246,7 +1266,7 @@ impl<'a> Emitter<'a> {
|
||||
} else {
|
||||
format!("gex_fmaf({a}, {b}, {c})")
|
||||
};
|
||||
self.line(format!("const {} {d} = ({}){e};", cty(*ty), cty(*ty)));
|
||||
self.line(format!("{} = ({}){e};", self.decl(*ty, &d), cty(*ty)));
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -1254,54 +1274,100 @@ impl<'a> Emitter<'a> {
|
||||
if func.clobbers_guest_state() {
|
||||
self.flush();
|
||||
}
|
||||
self.line(format!("const {} {d} = ({}){call};", cty(*ty), cty(*ty)));
|
||||
self.line(format!("{} = ({}){call};", self.decl(*ty, &d), cty(*ty)));
|
||||
if func.clobbers_guest_state() {
|
||||
self.invalidate();
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if let Expr::Mx { mx, e } = expr {
|
||||
// `e`'s own C, then the exact import when the bits ask for it:
|
||||
// a rounding other than to nearest, or FTZ or DAZ with a float
|
||||
// operand or the result denormal or the smallest normal (see
|
||||
// `runtime/mx.h`).
|
||||
let fast = self.rhs(idx, e)?;
|
||||
let m = self.v(*mx);
|
||||
let plan = MxPlan::of(e);
|
||||
let w = |t: Ty| if t == Ty::I64 { 8 } else { 4 };
|
||||
let (a, b, in_bytes, out_bytes) = match plan {
|
||||
MxPlan::Scalar { a, b, in_ty, .. } => (a, b, w(in_ty), w(ty)),
|
||||
MxPlan::Lanes { a, b, in_bytes, out_bytes, .. } => (a, b, in_bytes, out_bytes),
|
||||
};
|
||||
let tiny = |v: String, bytes: u32| {
|
||||
if ty == Ty::V128 {
|
||||
format!("gex_mx_tiny_v({v}, {bytes})")
|
||||
} else if bytes == 8 {
|
||||
format!("gex_mx_tiny64((uint64_t){v})")
|
||||
} else {
|
||||
format!("gex_mx_tiny32((uint32_t){v})")
|
||||
match self.mx_mode {
|
||||
MxMode::Own => {
|
||||
let own = self.rhs(idx, e)?;
|
||||
self.line(format!("{} = {own};", self.decl(ty, &d)));
|
||||
}
|
||||
};
|
||||
let mut redo = format!("((uint32_t){m} & 0x6000u) != 0");
|
||||
if plan.float_operands() {
|
||||
for v in [Some(a), b].into_iter().flatten() {
|
||||
redo += &format!(" || {}", tiny(self.v(v), in_bytes));
|
||||
MxMode::Exact => {
|
||||
let exact = self.mx_exact(*mx, e);
|
||||
self.line(format!("{} = {exact};", self.decl(ty, &d)));
|
||||
}
|
||||
MxMode::Each => {
|
||||
let own = self.rhs(idx, e)?;
|
||||
let m = self.v(*mx);
|
||||
let exact = self.mx_exact(*mx, e);
|
||||
let decl = if self.predeclared { String::new() } else { format!("{} ", cty(ty)) };
|
||||
self.line(format!("{decl}{d} = {own};"));
|
||||
self.line(format!(
|
||||
"if (__builtin_expect(((uint32_t){m} & 0x{:x}u) != 0, 0)) {d} = {exact};",
|
||||
MX_SPECIAL
|
||||
));
|
||||
}
|
||||
}
|
||||
redo += &format!(" || {}", tiny(d.clone(), out_bytes));
|
||||
let slow = self.mx_slow(*mx, e);
|
||||
self.line(format!("{} {d} = {fast};", cty(ty)));
|
||||
self.line(format!(
|
||||
"if (__builtin_expect(((uint32_t){m} & 0x{:x}u) != 0, 0) && ({redo})) {d} = {slow};",
|
||||
MX_SPECIAL
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
let rhs = self.rhs(idx, expr)?;
|
||||
self.line(format!("const {} {d} = {rhs};", cty(ty)));
|
||||
self.line(format!("{} = {rhs};", self.decl(ty, &d)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The left of a definition of `d`: a declaration, or the name alone
|
||||
/// when it was declared ahead ([`Emitter::stmts`]).
|
||||
fn decl(&self, ty: Ty, d: &str) -> String {
|
||||
if self.predeclared {
|
||||
d.to_string()
|
||||
} else {
|
||||
format!("const {} {d}", cty(ty))
|
||||
}
|
||||
}
|
||||
|
||||
/// A block's statements, each through `one`. Its `Expr::Mx`s go in runs
|
||||
/// of up to [`MX_RUN`], each from its first to its last: a run tests
|
||||
/// MXCSR's rounding control once and is emitted twice, with the
|
||||
/// operations' own C (taken when it is to nearest) and with their exact
|
||||
/// calls. A run's values are declared ahead, so both copies define them
|
||||
/// for what follows (the registers' locals are the function's). Short
|
||||
/// runs keep clang's work per basic block near what the block alone
|
||||
/// costs. A block that writes MXCSR or makes a host call (which may
|
||||
/// change it) tests each operation instead.
|
||||
pub(crate) fn stmts(&mut self, stmts: &[Stmt], mut one: impl FnMut(&mut Self, usize) -> Result<()>) -> Result<()> {
|
||||
let mxs: Vec<usize> =
|
||||
stmts.iter().enumerate().filter(|(_, s)| matches!(s, Stmt::Def { expr: Expr::Mx { .. }, .. })).map(|(k, _)| k).collect();
|
||||
let once = !mxs.is_empty()
|
||||
&& !stmts.iter().any(|s| {
|
||||
matches!(
|
||||
s,
|
||||
Stmt::WriteReg { reg: GuestReg::Mxcsr, .. } | Stmt::HostCall { .. } | Stmt::Def { expr: Expr::HostCall { .. }, .. }
|
||||
)
|
||||
});
|
||||
if !once {
|
||||
return (0..stmts.len()).try_for_each(|k| one(self, k));
|
||||
}
|
||||
let m = self.reg(GuestReg::Mxcsr);
|
||||
let mut next = 0;
|
||||
for chunk in mxs.chunks(MX_RUN) {
|
||||
let run = chunk[0]..chunk[chunk.len() - 1] + 1;
|
||||
(next..run.start).try_for_each(|k| one(self, k))?;
|
||||
for s in &stmts[run.clone()] {
|
||||
if let Stmt::Def { dst, expr } = s {
|
||||
self.line(format!("{} {};", cty(expr.ty()), self.v(*dst)));
|
||||
}
|
||||
}
|
||||
let regs = self.regs.clone();
|
||||
self.predeclared = true;
|
||||
self.line(format!("if (__builtin_expect(((uint32_t){m} & 0x{:x}u) != 0, 0)) {{", MX_SPECIAL));
|
||||
self.mx_mode = MxMode::Exact;
|
||||
run.clone().try_for_each(|k| one(self, k))?;
|
||||
self.line("} else {");
|
||||
self.regs = regs;
|
||||
self.mx_mode = MxMode::Own;
|
||||
run.clone().try_for_each(|k| one(self, k))?;
|
||||
self.line("}");
|
||||
self.mx_mode = MxMode::Each;
|
||||
self.predeclared = false;
|
||||
next = run.end;
|
||||
}
|
||||
(next..stmts.len()).try_for_each(|k| one(self, k))
|
||||
}
|
||||
|
||||
/// The C expression of `expr`, defining value `idx`.
|
||||
fn rhs(&mut self, idx: usize, expr: &Expr) -> Result<String> {
|
||||
Ok(match expr {
|
||||
@@ -1703,13 +1769,14 @@ impl<'a> Emitter<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// [`Expr::Mx`]'s slow path: the `mx` import through `runtime/mx.h`.
|
||||
fn mx_slow(&self, mx: Value, e: &Expr) -> String {
|
||||
/// [`Expr::Mx`] computed exactly: `gex_mx_s` or `gex_mx_v`
|
||||
/// (`runtime/mx.h`).
|
||||
fn mx_exact(&self, mx: Value, e: &Expr) -> String {
|
||||
let m = self.v(mx);
|
||||
match MxPlan::of(e) {
|
||||
MxPlan::Scalar { code, a, b, .. } => {
|
||||
let b = b.map_or("0".to_string(), |b| format!("(uint64_t){}", self.v(b)));
|
||||
let r = format!("gex_mx_scalar(ctx, {code}, (uint64_t){}, {b}, (uint32_t){m})", self.v(a));
|
||||
let r = format!("gex_mx_s(ctx, {code}, (uint64_t){}, {b}, (uint32_t){m})", self.v(a));
|
||||
if e.ty() == Ty::I32 {
|
||||
format!("(uint32_t){r}")
|
||||
} else {
|
||||
@@ -1717,8 +1784,9 @@ impl<'a> Emitter<'a> {
|
||||
}
|
||||
}
|
||||
MxPlan::Lanes { code, a, b, n, in_bytes, out_bytes } => {
|
||||
let f = n | if in_bytes == 8 { 16 } else { 0 } | if out_bytes == 8 { 32 } else { 0 };
|
||||
let b = b.map_or("wasm_i64x2_const(0, 0)".to_string(), |b| self.v(b));
|
||||
format!("gex_mx_lanes(ctx, {code}, {}, {b}, {n}, {in_bytes}, {out_bytes}, (uint32_t){m})", self.v(a))
|
||||
format!("gex_mx_v(ctx, {code}, {}, {b}, {f}u, (uint32_t){m})", self.v(a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1696,7 +1696,8 @@ fn ftab_call(e: &mut Emitter<'_>, t: &str, depth: &str, need: &str, ret: &str, f
|
||||
/// Emit a block's statements, telling the emitter where each resumable
|
||||
/// fault statement (`int3`'s `HostCall`) carries on.
|
||||
fn block_stmts(e: &mut Emitter<'_>, b: &FBlock) -> Result<()> {
|
||||
for (k, s) in b.stmts.iter().enumerate() {
|
||||
e.stmts(&b.stmts, |e, k| {
|
||||
let s = &b.stmts[k];
|
||||
e.fault_next = match s {
|
||||
Stmt::HostCall { func: HostFunc::Breakpoint | HostFunc::Illegal, .. } if e.fault_exit.is_some() => {
|
||||
let pc = b.pcs[k];
|
||||
@@ -1704,8 +1705,8 @@ fn block_stmts(e: &mut Emitter<'_>, b: &FBlock) -> Result<()> {
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
e.stmt(s)?;
|
||||
}
|
||||
e.stmt(s)
|
||||
})?;
|
||||
e.fault_next = None;
|
||||
Ok(())
|
||||
}
|
||||
@@ -2718,4 +2719,77 @@ b2 0x4007..0x4008 insts [4007]:
|
||||
crate::toolchain::compile_one(&dir, &dir.join("u.c"), &dir.join("u.o"), None).unwrap();
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
/// `0x5000`: one block of `n` SSE operations, vectors of each lane
|
||||
/// shape and scalars, each on the last's result; under MXCSR
|
||||
/// (`Expr::Mx`, as the lifter makes them under `gex_ir::mxcsr_exact`)
|
||||
/// when `mx`, else the operations alone (as it makes them without).
|
||||
fn sse_heavy(n: usize, mx: bool) -> String {
|
||||
let mut b = String::from(" @5000 v0 = read mxcsr\n @5000 v1 = read q0\n @5000 v2 = read q1\n @5000 v3 = read x0\n @5000 v4 = read x1\n");
|
||||
let (mut q, mut x, mut v) = (1, 3, 5);
|
||||
for k in 0..n {
|
||||
let e = match k % 6 {
|
||||
0 => format!("vlanebin Add F32 v{q} v2"),
|
||||
1 => format!("vlanebin Mul F64 v{q} v2"),
|
||||
2 => format!("vlaneun Sqrt F32 v{q}"),
|
||||
3 => format!("vcvt DemoteF64 F64 v{q} -"),
|
||||
4 => format!("fbin Div f64 v{x} v4"),
|
||||
_ => format!("fun Sqrt f64 v{x}"),
|
||||
};
|
||||
let e = if mx { format!("mx v0 {e}") } else { e };
|
||||
b += &format!(" @5000 v{v} = {e}\n");
|
||||
if k % 6 < 4 {
|
||||
q = v;
|
||||
} else {
|
||||
x = v;
|
||||
}
|
||||
v += 1;
|
||||
}
|
||||
b += &format!(" @5000 write q0 v{q}\n @5000 write x0 v{x}\n @5000 return v{x}\n");
|
||||
format!("func 0x5000 - entries [] form memory values {v}\nb0 0x5000..0x5001 insts [5000]:\n{b}")
|
||||
}
|
||||
|
||||
/// The function tier's C of `sse_heavy(n, mx)`'s function.
|
||||
fn sse_heavy_c(n: usize, mx: bool) -> (String, String) {
|
||||
let f = gex_ir::text::parse(&sse_heavy(n, mx)).unwrap();
|
||||
let known: HashSet<u64> = [0x5000].into_iter().collect();
|
||||
let mut unit = FnUnit::new();
|
||||
unit.add(&f, &known, &HashMap::new(), &HashMap::new()).unwrap();
|
||||
let c = unit.source();
|
||||
let body = c[c.find(&format!("{}(", body_name(0x5000))).unwrap()..].to_string();
|
||||
(c, body)
|
||||
}
|
||||
|
||||
/// Without MXCSR, an SSE operation's C is the operation's own and
|
||||
/// nothing else. Under it, a block's run of up to 32 of them tests the
|
||||
/// rounding control once and is the operations' own C or their exact calls, out
|
||||
/// of line in `runtime/mx.h` (an operation is one call however many
|
||||
/// lanes): no more than twice the C. A test and an inline slow path
|
||||
/// per operation was twice the C as well, but as thousands of branches
|
||||
/// in one function, which took clang over 32 GB.
|
||||
#[test]
|
||||
fn sse_operations_under_mxcsr_test_once_a_block() {
|
||||
if !gex_abi::X86 {
|
||||
return; // MXCSR is x86's
|
||||
}
|
||||
let n = 600;
|
||||
let (_, plain) = sse_heavy_c(n, false);
|
||||
assert!(!plain.contains("gex_mx"), "no MXCSR without it");
|
||||
let (c, body) = sse_heavy_c(n, true);
|
||||
let exact = body.matches("gex_mx_s(").count() + body.matches("gex_mx_v(").count();
|
||||
assert_eq!(exact, n, "one out-of-line exact call per operation");
|
||||
for inline in ["gex_mx(", "replace_lane", "extract_lane"] {
|
||||
assert!(!body.contains(inline), "`{inline}` is out of line, not in the function");
|
||||
}
|
||||
let tests = |c: &str| c.matches("__builtin_expect").count();
|
||||
assert_eq!(tests(&body), tests(&plain) + n.div_ceil(32), "one rounding-control test for each run of 32");
|
||||
let (with, without) = (body.len(), plain.len());
|
||||
assert!(with < 2 * without + 64 * n, "{with} bytes of C under MXCSR, {without} without");
|
||||
eprintln!("{with} bytes of C under MXCSR, {without} without ({n} operations)");
|
||||
let dir = std::env::temp_dir().join(format!("gex-cgen-mx-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("gex_abi.h"), crate::header::header()).unwrap();
|
||||
std::fs::write(dir.join("u.c"), &c).unwrap();
|
||||
crate::toolchain::compile_one(&dir, &dir.join("u.c"), &dir.join("u.o"), None).unwrap();
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,9 @@ fn unit_inputs() -> String {
|
||||
// The null guard, a debugging opt-in: named only when on, so the
|
||||
// units of a build without it keep their keys.
|
||||
let base = if gex_emit::tier1::null_guard_on() { format!("{base}:nullguard:{:x}", gex_abi::NULL_GUARD) } else { base };
|
||||
// SSE under MXCSR (`gex_ir::mxcsr_exact`), a per-program opt-in, the
|
||||
// same way: the units with it and without it never mix.
|
||||
let base = if gex_ir::mxcsr_exact() { format!("{base}:mxcsr") } else { base };
|
||||
// So is the guest memory (docs/mem64.md): a wasm64 unit is a different
|
||||
// module. Nothing is added for wasm32, whose keys stay as they were.
|
||||
if gex_abi::mem64() {
|
||||
|
||||
@@ -741,10 +741,16 @@ impl ModuleBuilder {
|
||||
vec![ValType::I64],
|
||||
));
|
||||
|
||||
let t_mx = m.func_type(FuncType::new(
|
||||
vec![p, ValType::I32, ValType::I64, ValType::I64, ValType::I32],
|
||||
vec![ValType::I64],
|
||||
));
|
||||
// The MXCSR import only for a program lifted under MXCSR
|
||||
// (`gex_ir::mxcsr_exact`): without it, the module is as if it did
|
||||
// not exist, and nothing calls it.
|
||||
let mx_on = gex_ir::mxcsr_exact();
|
||||
let t_mx = mx_on.then(|| {
|
||||
m.func_type(FuncType::new(
|
||||
vec![p, ValType::I32, ValType::I64, ValType::I64, ValType::I32],
|
||||
vec![ValType::I64],
|
||||
))
|
||||
});
|
||||
|
||||
let im = IMPORT_MODULE;
|
||||
let imports = Imports {
|
||||
@@ -758,7 +764,7 @@ impl ModuleBuilder {
|
||||
safepoint: m.import_func(im, imports::SAFEPOINT, t_syscall),
|
||||
fma: m.import_func(im, imports::FMA, t_fma),
|
||||
x87: m.import_func(im, imports::X87, t_x87),
|
||||
mx: m.import_func(im, imports::MX, t_mx),
|
||||
mx: t_mx.map_or(u32::MAX, |t| m.import_func(im, imports::MX, t)),
|
||||
};
|
||||
|
||||
// The memory is the host's and shared, so every guest thread acts on
|
||||
|
||||
@@ -91,13 +91,6 @@ mod simd {
|
||||
pub const I32X4_EXTADD_PAIRWISE_I16X8_S: u32 = 0x7e;
|
||||
pub const I32X4_EXTADD_PAIRWISE_I16X8_U: u32 = 0x7f;
|
||||
pub const V128_AND: u32 = 0x4e;
|
||||
pub const V128_ANY_TRUE: u32 = 0x53;
|
||||
pub const I32X4_EQ: u32 = 0x37;
|
||||
pub const I32X4_SHR_U: u32 = 0xad;
|
||||
pub const I32X4_SUB: u32 = 0xb1;
|
||||
pub const I64X2_EQ: u32 = 0xd6;
|
||||
pub const I64X2_SHR_U: u32 = 0xcd;
|
||||
pub const I64X2_SUB: u32 = 0xd1;
|
||||
pub const V128_ANDNOT: u32 = 0x4f;
|
||||
pub const V128_OR: u32 = 0x50;
|
||||
pub const V128_XOR: u32 = 0x51;
|
||||
@@ -258,11 +251,13 @@ fn t1_fold() -> bool {
|
||||
}
|
||||
|
||||
/// The emitter switches that change tier-1's bytes without showing in the
|
||||
/// translation options, for a cache key (GEX_INLINE_DISPATCH, GEX_T1_FOLD):
|
||||
/// translation options, for a cache key (GEX_INLINE_DISPATCH, GEX_T1_FOLD,
|
||||
/// GEX_NULL_GUARD, GEX_MXCSR):
|
||||
/// natively they are read from the environment, which the module cache's key
|
||||
/// did not see, so a run with one set served its modules to runs without.
|
||||
pub fn knobs() -> u8 {
|
||||
inline_dispatch() as u8 | (t1_fold() as u8) << 1 | (gex_abi::kuser_fold() as u8) << 2 | (null_guard_on() as u8) << 3
|
||||
| (gex_ir::mxcsr_exact() as u8) << 4
|
||||
}
|
||||
|
||||
/// Whether the translators put the null guard (`gex_abi::NULL_GUARD`) on
|
||||
@@ -3144,11 +3139,8 @@ impl<'a> Emitter<'a> {
|
||||
self.value_local[idx] = Some(local);
|
||||
}
|
||||
|
||||
/// An [`Expr::Mx`]: `e`'s own lowering, kept when `mx` has none of
|
||||
/// [`MX_SPECIAL`]'s bits; and kept as well under FTZ or DAZ alone
|
||||
/// (rounding to nearest) when no float operand or result is a denormal
|
||||
/// or the smallest normal (one a tiny result may have rounded to), as
|
||||
/// then neither flush has anything to do. Otherwise the pure `mx`
|
||||
/// An [`Expr::Mx`]: `e`'s own lowering, kept when `mx`'s rounding
|
||||
/// control ([`MX_SPECIAL`]) is to nearest; otherwise the pure `mx`
|
||||
/// import, once for a scalar and once per lane ([`MxPlan`]). Either
|
||||
/// way the result is left in one integer-typed local.
|
||||
fn emit_mx(&mut self, dst: Value, mx: Value, e: &Expr) {
|
||||
@@ -3164,26 +3156,6 @@ impl<'a> Emitter<'a> {
|
||||
self.f.i32_const(MX_SPECIAL as i32);
|
||||
self.f.byte(I32_AND);
|
||||
self.f.if_void();
|
||||
self.push(mx);
|
||||
self.f.i32_const(0x6000);
|
||||
self.f.byte(I32_AND);
|
||||
let w = |t: Ty| if t == Ty::I64 { 8 } else { 4 };
|
||||
let (a, b, in_bytes, out_bytes) = match plan {
|
||||
MxPlan::Scalar { a, b, in_ty, .. } => (a, b, w(in_ty), w(ty)),
|
||||
MxPlan::Lanes { a, b, in_bytes, out_bytes, .. } => (a, b, in_bytes, out_bytes),
|
||||
};
|
||||
let lanes = ty == Ty::V128;
|
||||
if plan.float_operands() {
|
||||
for v in [Some(a), b].into_iter().flatten() {
|
||||
self.push(v);
|
||||
self.emit_tiny_check(lanes, in_bytes);
|
||||
self.f.byte(I32_OR);
|
||||
}
|
||||
}
|
||||
self.f.local_get(res);
|
||||
self.emit_tiny_check(lanes, out_bytes);
|
||||
self.f.byte(I32_OR);
|
||||
self.f.if_void();
|
||||
// The import's operand: an integer of `in_bytes` (from lane `lane`
|
||||
// of a vector), as an i64.
|
||||
let operand = |this: &mut Self, v: Value, lane: Option<(u32, u32)>, in_ty: Ty| {
|
||||
@@ -3239,7 +3211,6 @@ impl<'a> Emitter<'a> {
|
||||
}
|
||||
}
|
||||
self.f.end();
|
||||
self.f.end();
|
||||
self.value_ty[idx] = ty;
|
||||
if let Some(f) = self.value_fp.get_mut(idx) {
|
||||
*f = None;
|
||||
@@ -3248,56 +3219,6 @@ impl<'a> Emitter<'a> {
|
||||
self.value_local[idx] = Some(res);
|
||||
}
|
||||
|
||||
/// Replace the float bits on the stack (a scalar of `bytes`, or a vector
|
||||
/// of `bytes`-wide lanes) with an i32, nonzero when it (a lane) is a
|
||||
/// denormal or the smallest normal: `(|x| - 1) >> fraction bits == 0`.
|
||||
fn emit_tiny_check(&mut self, lanes: bool, bytes: u32) {
|
||||
use code::*;
|
||||
let frac = if bytes == 8 { 52 } else { 23 };
|
||||
let mut abs = [0xffu8; 16];
|
||||
let mut one = [0u8; 16];
|
||||
for k in 0..16 / bytes as usize {
|
||||
abs[k * bytes as usize + bytes as usize - 1] = 0x7f;
|
||||
one[k * bytes as usize] = 1;
|
||||
}
|
||||
match (lanes, bytes) {
|
||||
(false, 8) => {
|
||||
self.f.i64_const(i64::MAX);
|
||||
self.f.byte(I64_AND);
|
||||
self.f.i64_const(1);
|
||||
self.f.byte(I64_SUB);
|
||||
self.f.i64_const(frac);
|
||||
self.f.byte(I64_SHR_U);
|
||||
self.f.byte(I64_EQZ);
|
||||
}
|
||||
(false, _) => {
|
||||
self.f.i32_const(i32::MAX);
|
||||
self.f.byte(I32_AND);
|
||||
self.f.i32_const(1);
|
||||
self.f.byte(I32_SUB);
|
||||
self.f.i32_const(frac as i32);
|
||||
self.f.byte(I32_SHR_U);
|
||||
self.f.byte(I32_EQZ);
|
||||
}
|
||||
(true, _) => {
|
||||
let (sub, shr, eq) = if bytes == 8 {
|
||||
(simd::I64X2_SUB, simd::I64X2_SHR_U, simd::I64X2_EQ)
|
||||
} else {
|
||||
(simd::I32X4_SUB, simd::I32X4_SHR_U, simd::I32X4_EQ)
|
||||
};
|
||||
self.f.v128_const(&abs);
|
||||
self.f.simd(simd::V128_AND);
|
||||
self.f.v128_const(&one);
|
||||
self.f.simd(sub);
|
||||
self.f.i32_const(frac as i32);
|
||||
self.f.simd(shr);
|
||||
self.f.v128_const(&[0; 16]);
|
||||
self.f.simd(eq);
|
||||
self.f.simd(simd::V128_ANY_TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reinterpret the integer bit pattern on top of the stack as a float.
|
||||
/// Reverse the bytes of the value on the stack. Wasm
|
||||
/// has no byte swap: 32 bits is two rotates, two masks and an or; 16 bits
|
||||
|
||||
@@ -59,7 +59,9 @@ fn host_imports(m: &mut Module) -> Imports {
|
||||
vec![p, ValType::I32, ValType::I64, ValType::I32, ValType::I64, ValType::I32, ValType::I32],
|
||||
vec![ValType::I64],
|
||||
));
|
||||
let t_mx = m.func_type(FuncType::new(vec![p, ValType::I32, ValType::I64, ValType::I64, ValType::I32], vec![ValType::I64]));
|
||||
// The MXCSR import only under `gex_ir::mxcsr_exact` (tier-1's `ModuleBuilder`).
|
||||
let t_mx = gex_ir::mxcsr_exact()
|
||||
.then(|| m.func_type(FuncType::new(vec![p, ValType::I32, ValType::I64, ValType::I64, ValType::I32], vec![ValType::I64])));
|
||||
let im = IMPORT_MODULE;
|
||||
let out = Imports {
|
||||
syscall: m.import_func(im, imports::SYSCALL, t_syscall),
|
||||
@@ -72,7 +74,7 @@ fn host_imports(m: &mut Module) -> Imports {
|
||||
safepoint: m.import_func(im, imports::SAFEPOINT, t_syscall),
|
||||
fma: m.import_func(im, imports::FMA, t_fma),
|
||||
x87: m.import_func(im, imports::X87, t_x87),
|
||||
mx: m.import_func(im, imports::MX, t_mx),
|
||||
mx: t_mx.map_or(u32::MAX, |t| m.import_func(im, imports::MX, t)),
|
||||
dispatch: m.import_func(gex_cgen::splice::DISPATCH_MODULE, gex_cgen::splice::DISPATCH_NAME, block_ty),
|
||||
block_ty,
|
||||
};
|
||||
@@ -387,11 +389,13 @@ pub fn build_runtime(
|
||||
let mut funcs: Vec<Function> = funcs.to_vec();
|
||||
loop {
|
||||
let loops: Vec<Vec<Loop>> = funcs.iter().map(|f| if outline { cf::outer_loops(f) } else { Vec::new() }).collect();
|
||||
// Imports: tier-1's eleven host functions, in `host_imports`' order.
|
||||
const N_IMPORTS: u32 = 11;
|
||||
// Imports: tier-1's host functions, in `host_imports`' order: ten,
|
||||
// and the MXCSR one under `gex_ir::mxcsr_exact`.
|
||||
let mx_on = gex_ir::mxcsr_exact();
|
||||
let n_imports: u32 = 10 + mx_on as u32;
|
||||
let (block_ty, t_syscall, t_miss, t_pc_i32, t_sysreg_read, t_pc_i32_i64, t_fma, t_x87, body_ty, loop_ty, t_mx) =
|
||||
(0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
|
||||
let mut next = N_IMPORTS;
|
||||
let mut next = n_imports;
|
||||
let mut bodies: HashMap<u64, u32> = HashMap::new();
|
||||
let mut lfns: HashMap<(u64, usize), u32> = HashMap::new();
|
||||
let mut order: Vec<(u32, u32)> = Vec::new(); // (function index, type)
|
||||
@@ -419,7 +423,7 @@ pub fn build_runtime(
|
||||
safepoint: 7,
|
||||
fma: 8,
|
||||
x87: 9,
|
||||
mx: 10,
|
||||
mx: if mx_on { 10 } else { u32::MAX },
|
||||
block_ty,
|
||||
entry_ty: 0,
|
||||
blocks: &no_blocks,
|
||||
@@ -472,7 +476,9 @@ pub fn build_runtime(
|
||||
types.ty().function([i32_, i32_, i64_, i32_, i64_, i32_, i32_], [i64_]);
|
||||
types.ty().function([i32_, i32_, i32_, i64_], [i32_]);
|
||||
types.ty().function([i32_, i32_, i32_, i64_, i64_], [i32_]);
|
||||
types.ty().function([i32_, i32_, i64_, i64_, i32_], [i64_]);
|
||||
if mx_on {
|
||||
types.ty().function([i32_, i32_, i64_, i64_, i32_], [i64_]);
|
||||
}
|
||||
m.section(&types);
|
||||
let mut imports = ImportSection::new();
|
||||
let im = gex_abi::IMPORT_MODULE;
|
||||
@@ -487,10 +493,12 @@ pub fn build_runtime(
|
||||
(imports::SAFEPOINT, t_syscall),
|
||||
(imports::FMA, t_fma),
|
||||
(imports::X87, t_x87),
|
||||
(imports::MX, t_mx),
|
||||
] {
|
||||
imports.import(im, name, EntityType::Function(ty));
|
||||
}
|
||||
if mx_on {
|
||||
imports.import(im, imports::MX, EntityType::Function(t_mx));
|
||||
}
|
||||
imports.import(
|
||||
im,
|
||||
exports::MEMORY,
|
||||
|
||||
+27
-14
@@ -39,6 +39,25 @@ pub fn take_kuser_folds() -> u64 {
|
||||
KUSER_FOLDS.with(|c| c.replace(0))
|
||||
}
|
||||
|
||||
static MXCSR_EXACT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
/// SSE under the guest's MXCSR rounding control, a per-program opt-in: set
|
||||
/// before anything is translated (the page's `?mxcsr=1`, GEX_MXCSR=1
|
||||
/// natively). On, an SSE operation MXCSR rounds is lifted as [`Expr::Mx`]
|
||||
/// and rounds as the rounding control says in every tier. Off, it is the
|
||||
/// operation alone, rounded to nearest, and every tier's output and every
|
||||
/// key are as if MXCSR did not exist. FTZ and DAZ are honoured in neither.
|
||||
pub fn set_mxcsr_exact(on: bool) {
|
||||
MXCSR_EXACT.store(on, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Whether SSE is lifted under MXCSR ([`set_mxcsr_exact`]).
|
||||
pub fn mxcsr_exact() -> bool {
|
||||
static ENV: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ENV.get_or_init(|| std::env::var_os("GEX_MXCSR").is_some_and(|v| v != "0"))
|
||||
|| MXCSR_EXACT.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Value(pub u32);
|
||||
|
||||
@@ -473,11 +492,11 @@ pub enum Expr {
|
||||
a: Value,
|
||||
},
|
||||
/// `e` computed as x86's SSE computes it under the `I32` MXCSR value
|
||||
/// `mx`: rounded by its rounding control (bits 13-14) instead of to
|
||||
/// nearest, a denormal operand read as a zero of its sign under DAZ
|
||||
/// (bit 6), a denormal result written as one under FTZ (bit 15). With
|
||||
/// none of those bits set ([`MX_SPECIAL`]) it is `e` exactly, so a
|
||||
/// backend lowers it as `e` behind that one test.
|
||||
/// `mx`'s rounding control (bits 13-14, [`MX_SPECIAL`]) instead of to
|
||||
/// nearest. FTZ and DAZ are not honoured: denormals are kept, as wasm
|
||||
/// keeps them. With the rounding to nearest it is `e` exactly, so a
|
||||
/// backend lowers it as `e` behind that one test (for a run of them,
|
||||
/// one test for the run).
|
||||
///
|
||||
/// `e` is one of [`Expr::mx_able`]'s: a float add, subtract, multiply,
|
||||
/// divide or square root, a round to nearest (an integer value, the
|
||||
@@ -487,8 +506,9 @@ pub enum Expr {
|
||||
Mx { mx: Value, e: Box<Expr> },
|
||||
}
|
||||
|
||||
/// The MXCSR bits [`Expr::Mx`] honours: rounding control, FTZ, DAZ.
|
||||
pub const MX_SPECIAL: u32 = 0xe040;
|
||||
/// The MXCSR bits [`Expr::Mx`] honours: the rounding control. The exact
|
||||
/// import (`gex_abi::imports::MX`) is given only these.
|
||||
pub const MX_SPECIAL: u32 = 0x6000;
|
||||
|
||||
/// How an [`Expr::Mx`]'s operation is computed off its fast path: by
|
||||
/// `gex_f80::sse::op` with `code`, on raw bits, once for a scalar or once
|
||||
@@ -567,13 +587,6 @@ impl MxPlan {
|
||||
_ => panic!("MxPlan::of: {e:?} is not an operation MXCSR rounds"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The operands are floats (DAZ reads them), not a conversion's integers.
|
||||
pub fn float_operands(&self) -> bool {
|
||||
use gex_f80::sse::*;
|
||||
let (MxPlan::Scalar { code, .. } | MxPlan::Lanes { code, .. }) = *self;
|
||||
!matches!(code & 0xf, FROM_I32 | FROM_I64)
|
||||
}
|
||||
}
|
||||
|
||||
/// Scalar floating-point operations.
|
||||
|
||||
@@ -130,6 +130,30 @@ mod tests {
|
||||
assert_eq!(strops(&b), 0, "{}", print::block(&b));
|
||||
}
|
||||
|
||||
/// SSE is lifted under MXCSR only for a program that asks
|
||||
/// (`gex_ir::mxcsr_exact`): without it, `addps`, `mulsd`, `sqrtps` and
|
||||
/// `cvtpd2ps` are their operations alone, with no `Expr::Mx` and no
|
||||
/// read of MXCSR, as if MXCSR did not exist; with it, each is one.
|
||||
#[test]
|
||||
fn sse_is_under_mxcsr_only_when_asked() {
|
||||
let mxs = |b: &Block| {
|
||||
b.stmts.iter().filter(|s| matches!(s, Stmt::Def { expr: Expr::Mx { .. }, .. })).count()
|
||||
};
|
||||
let reads = |b: &Block| {
|
||||
b.stmts.iter().filter(|s| matches!(s, Stmt::Def { expr: Expr::ReadReg(GuestReg::Mxcsr), .. })).count()
|
||||
};
|
||||
// addps xmm0, xmm1 ; mulsd xmm2, xmm1 ; sqrtps xmm3, xmm1 ; cvtpd2ps xmm4, xmm1 ; ret
|
||||
let code = &[0x0f, 0x58, 0xc1, 0xf2, 0x0f, 0x59, 0xd1, 0x0f, 0x51, 0xd9, 0x66, 0x0f, 0x5a, 0xe1, 0xc3];
|
||||
if !gex_ir::mxcsr_exact() {
|
||||
let b = lift_block(bytes(0x1000, code), 0x1000, BlockLimits::default());
|
||||
assert_eq!((mxs(&b), reads(&b)), (0, 0), "{}", print::block(&b));
|
||||
}
|
||||
gex_ir::set_mxcsr_exact(true);
|
||||
let b = lift_block(bytes(0x1000, code), 0x1000, BlockLimits::default());
|
||||
gex_ir::set_mxcsr_exact(false);
|
||||
assert_eq!(mxs(&b), 4, "{}", print::block(&b));
|
||||
}
|
||||
|
||||
/// `fpatan` (d9 f3) is handed to the host rather than raising SIGILL:
|
||||
/// i386 musl's `atan2` is this one instruction.
|
||||
#[test]
|
||||
|
||||
@@ -155,11 +155,12 @@ impl<'a> Lifter<'a> {
|
||||
self.mx(Expr::VLaneUn { op, lane, a })
|
||||
}
|
||||
|
||||
/// `e` as SSE computes it: under the guest's MXCSR (its rounding
|
||||
/// control, FTZ and DAZ) when it is an operation MXCSR controls
|
||||
/// ([`Expr::mx_able`]), else as it is.
|
||||
/// `e` as SSE computes it: under the guest's MXCSR rounding control
|
||||
/// when it is an operation MXCSR rounds
|
||||
/// ([`Expr::mx_able`]) and the program asked for it
|
||||
/// ([`gex_ir::mxcsr_exact`]), else as it is.
|
||||
fn mx(&mut self, e: Expr) -> Value {
|
||||
if !e.mx_able() {
|
||||
if !e.mx_able() || !gex_ir::mxcsr_exact() {
|
||||
return self.b.def(e);
|
||||
}
|
||||
let mx = self.b.read(GuestReg::Mxcsr);
|
||||
|
||||
@@ -1248,6 +1248,8 @@ export class Gex {
|
||||
if (globalThis.GEX_W3_SPLIT || this.#w64Split) this.#shim.exports.gex_set_w3_split?.(1);
|
||||
// `?nullguard=1` before anything is translated (gex_abi::null_guard).
|
||||
if (globalThis.GEX_NULL_GUARD) this.#shim.exports.gex_set_null_guard?.(1);
|
||||
// `?mxcsr=1` before anything is translated: SSE under MXCSR (gex_ir::mxcsr_exact).
|
||||
if (globalThis.GEX_MXCSR) this.#shim.exports.gex_set_mxcsr_exact?.(1);
|
||||
// `?pinregion=BYTES`: a project's libraries' region above the placement
|
||||
// window, before the first claim of arena (gex_abi::set_pin_region).
|
||||
if (globalThis.GEX_PIN_REGION) this.#shim.exports.gex_set_pin_region?.(BigInt(globalThis.GEX_PIN_REGION));
|
||||
|
||||
@@ -428,6 +428,8 @@ const host = new ProcessHost({
|
||||
w3Split: !!process.env.GEX_W3_SPLIT,
|
||||
// GEX_NULL_GUARD=1: as the page's ?nullguard=1 (gex_abi::null_guard).
|
||||
nullGuard: process.env.GEX_NULL_GUARD === '1' || undefined,
|
||||
// GEX_MXCSR=1: as the page's ?mxcsr=1 (gex_ir::mxcsr_exact).
|
||||
mxcsr: process.env.GEX_MXCSR === '1' || undefined,
|
||||
// GEX_PIN_REGION=BYTES: as the page's ?pinregion= (gex_abi::set_pin_region).
|
||||
pinRegion: process.env.GEX_PIN_REGION ? Number(process.env.GEX_PIN_REGION) : undefined,
|
||||
// GEX_SEH_REPORT=1: as the page's ?sehreport=1.
|
||||
|
||||
@@ -692,6 +692,8 @@ async function handle(msg) {
|
||||
if (msg.w3Split) globalThis.GEX_W3_SPLIT = 1;
|
||||
// `?nullguard=1`: the null guard (gex_abi::null_guard), a debugging opt-in.
|
||||
if (msg.nullGuard) globalThis.GEX_NULL_GUARD = 1;
|
||||
// `?mxcsr=1`: SSE under MXCSR (gex_ir::mxcsr_exact), a per-program opt-in.
|
||||
if (msg.mxcsr) globalThis.GEX_MXCSR = 1;
|
||||
// `?pinregion=BYTES`: the pin region (gex_abi::set_pin_region).
|
||||
if (msg.pinRegion) globalThis.GEX_PIN_REGION = msg.pinRegion;
|
||||
// `?sehreport=1`: crash reports name the SEH chain (a debugging opt-in).
|
||||
|
||||
@@ -2506,6 +2506,14 @@ pub extern "C" fn gex_set_null_guard(on: u32) {
|
||||
gex_abi::set_null_guard(on != 0);
|
||||
}
|
||||
|
||||
/// `gex_set_mxcsr_exact(1)` (`?mxcsr=1`, run-node's GEX_MXCSR=1), before
|
||||
/// anything is translated: SSE under the guest's MXCSR
|
||||
/// (`gex_ir::mxcsr_exact`), a per-program opt-in.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_set_mxcsr_exact(on: u32) {
|
||||
gex_ir::set_mxcsr_exact(on != 0);
|
||||
}
|
||||
|
||||
/// `?pinregion=BYTES`: the pin region an ejected project's libraries are
|
||||
/// placed in (`gex_abi::set_pin_region`), before this Worker takes anything
|
||||
/// from the arena.
|
||||
@@ -13956,11 +13964,11 @@ pub extern "C" fn gex_fma(_ctx: i32, a: i64, b: i64, c: i64, double: i32) -> i64
|
||||
}
|
||||
}
|
||||
|
||||
/// An SSE operation under a non-default MXCSR (`gex_abi::imports::MX`):
|
||||
/// exact, pure.
|
||||
/// An SSE operation under a rounding control other than to nearest
|
||||
/// (`gex_abi::imports::MX`): exact, pure; MXCSR's other bits ignored.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_mx(_ctx: i32, code: i32, a: i64, b: i64, mx: i32) -> i64 {
|
||||
gex_kernel::f80::sse::op(code as u32, a as u64, b as u64, mx as u32) as i64
|
||||
gex_kernel::f80::sse::op(code as u32, a as u64, b as u64, mx as u32 & gex_ir::MX_SPECIAL) as i64
|
||||
}
|
||||
|
||||
/// An x87 operation the inline fast path left to the host (see
|
||||
|
||||
@@ -5001,9 +5001,10 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
|
||||
})()) },
|
||||
)?;
|
||||
|
||||
// An SSE operation under a non-default MXCSR (`gex_abi::imports::MX`).
|
||||
// An SSE operation under a rounding control other than to nearest
|
||||
// (`gex_abi::imports::MX`); MXCSR's other bits ignored.
|
||||
linker.func_wrap(m, imports::MX, |_ctx: i32, code: i32, a: i64, b: i64, mx: i32| -> i64 {
|
||||
gex_kernel::f80::sse::op(code as u32, a as u64, b as u64, mx as u32) as i64
|
||||
gex_kernel::f80::sse::op(code as u32, a as u64, b as u64, mx as u32 & gex_ir::MX_SPECIAL) as i64
|
||||
})?;
|
||||
|
||||
// The counting flavor's table-call note (`gex_cgen::image::icall_count`):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* The rounding and denormal controls: SSE arithmetic and conversions under
|
||||
* each MXCSR rounding mode and with FTZ and DAZ, and x87 arithmetic and
|
||||
* stores under each rounding and precision control of the control word.
|
||||
/* The rounding controls: SSE arithmetic and conversions under each MXCSR
|
||||
* rounding mode (gex honours MXCSR's rounding control, not FTZ or DAZ), and
|
||||
* x87 arithmetic and stores under each rounding and precision control of
|
||||
* the control word.
|
||||
* Each line is one operation in one mode, a hash of its results over every
|
||||
* pair of a set of operands (denormals, values a rounding mode moves, ones
|
||||
* that overflow or underflow), compared with the hardware's.
|
||||
@@ -127,7 +128,7 @@ static void x87(uint16_t cw) {
|
||||
|
||||
int main(void) {
|
||||
#if defined(__x86_64__)
|
||||
static const uint32_t modes[] = {0x0000, 0x2000, 0x4000, 0x6000, 0x8000, 0x0040, 0x8040, 0xe040};
|
||||
static const uint32_t modes[] = {0x0000, 0x2000, 0x4000, 0x6000};
|
||||
for (unsigned k = 0; k < sizeof modes / sizeof modes[0]; k++) sse(modes[k]);
|
||||
#endif
|
||||
static const uint16_t cws[] = {0x037f, 0x077f, 0x0b7f, 0x0f7f, 0x007f, 0x027f, 0x0e7f, 0x047f};
|
||||
|
||||
@@ -221,9 +221,12 @@ SEED64=web/public/gex/seed.x86.m64.shared.wasm
|
||||
# descriptors block until a second thread's signal ends them with EINTR
|
||||
# (i386's pause is a call of its own).
|
||||
# fp-modes: SSE arithmetic, square roots, roundings and conversions under
|
||||
# each MXCSR rounding mode with FTZ and DAZ (wasm rounds only to nearest:
|
||||
# the other modes take the exact slow path), and x87 arithmetic and stores
|
||||
# under each rounding and precision control; under the function JIT too.
|
||||
# each MXCSR rounding mode (wasm rounds only to nearest: the other modes take
|
||||
# the exact slow path), and x87 arithmetic and stores
|
||||
# under each rounding and precision control; under the function JIT and in
|
||||
# the function tier's C too.
|
||||
# SSE under MXCSR's rounding control is a per-program opt-in (GEX_MXCSR=1,
|
||||
# the page's ?mxcsr=1).
|
||||
PAGE_PROGRAMS=${PAGE_PROGRAMS:-"fork-dirs dense-lazy dense-holes spawn-threads served-entries page-cross-entry tgkill-futex exec-tail mem64-io mem64-net mem64-console mem64-smc mem64-reserve retire-during-lift close-while-reading thread-signals signal-storm exec-signals clock-monotonic main-thread-exit pi-mutex tcp-loopback pwait-sigmask sa-restart thread-churn kernel-contend exec-futex sleep-threads sendmsg-rights scm-fork ioctl-threads fdtable-threads mmap-threads codemap-threads proc-threads fs-threads sig-state-threads lift-threads fdmisc-threads task-ids sock-threads exec-dethread fork-thread-ctx fork-maps-threads fork-twice-thread quiet-threads fdobj-threads cred-threads kill-threads sigwait-threads poll-timeout tid-threads mount-threads exit-threads many-workers threads-many stale-loop-head file-stale-loop stale-cross-page xattr mem64-wx mmap-coherent mmap-remap mmap-writeback null-access sig-regs-threads sig-pause fp-modes"}
|
||||
for src in "$HERE"/*.c; do
|
||||
name=$(basename "$src" .c)
|
||||
@@ -235,7 +238,9 @@ for src in "$HERE"/*.c; do
|
||||
# *smc*: self-modifying code, under the function JIT as well (which
|
||||
# must leave writable-and-executable code to tier-1's guards).
|
||||
tiers="t1"
|
||||
case $name in *smc* | copy-loop | null-access | sig-regs-threads | fp-modes) tiers="t1 fjit" ;; esac
|
||||
case $name in *smc* | copy-loop | null-access | sig-regs-threads) tiers="t1 fjit" ;; esac
|
||||
# fp-modes: the function tier's C as well.
|
||||
case $name in fp-modes) tiers="t1 fjit cgen" ;; esac
|
||||
case " $PAGE_PROGRAMS " in *" $name "*)
|
||||
if [ -f "$SEED64" ] && command -v node >/dev/null; then tiers="$tiers page"; fi ;;
|
||||
esac
|
||||
@@ -246,6 +251,7 @@ for src in "$HERE"/*.c; do
|
||||
case " ${TIERS:-$tier} " in *" $tier "*) ;; *) continue ;; esac
|
||||
flags=$gexflags
|
||||
[ $tier = fjit ] && flags="$flags --fjit"
|
||||
[ $tier = cgen ] && flags="$flags --cgen"
|
||||
extra=""
|
||||
[ "$target" = x86-linux-musl ] && extra="-Wl,--image-base=0x1000000"
|
||||
bin="$WORK/$name-$target"
|
||||
@@ -279,6 +285,8 @@ for src in "$HERE"/*.c; do
|
||||
[ $name = smc-race ] && envs="GEX_TEST_LIFT_DELAY_US=300"
|
||||
# null-access needs the null guard, a debug opt-in (gex_abi::null_guard).
|
||||
[ $name = null-access ] && envs="GEX_NULL_GUARD=1"
|
||||
# fp-modes needs SSE under MXCSR, a per-program opt-in (gex_ir::mxcsr_exact).
|
||||
[ $name = fp-modes ] && envs="GEX_MXCSR=1"
|
||||
# mem64-net reaches the host: the page gets a network of real TCP sockets.
|
||||
[ $name = mem64-net ] && envs="GEX_NETWORK=1"
|
||||
[ $name = page-cross-entry ] && [ $tier = page ] && envs="GEX_TEST_UNPUBLISHED_RETIRE=1"
|
||||
|
||||
@@ -979,6 +979,10 @@ export function setGlobals(get, b = {}) {
|
||||
// natively (gex_abi::null_guard). A debugging opt-in: off, tier 1 and the
|
||||
// units are as without it; on, 2-8% slower in tier 1.
|
||||
if (get('nullguard') === '1') globalThis.GEX_NULL_GUARD = 1;
|
||||
// mxcsr=1: SSE honours MXCSR's rounding control in every tier
|
||||
// (gex_ir::mxcsr_exact), for a program that needs it. Off, SSE rounds to
|
||||
// nearest, and tier 1 and the units are as without it.
|
||||
if (get('mxcsr') === '1') globalThis.GEX_MXCSR = 1;
|
||||
// pinregion=BYTES: address space above the placement window for the
|
||||
// project's libraries, each at its build's base (gex_abi::set_pin_region).
|
||||
if (get('pinregion')) globalThis.GEX_PIN_REGION = Number(get('pinregion'));
|
||||
|
||||
+4
-1
@@ -134,7 +134,8 @@ function xcacheName() {
|
||||
// `tso=arm` names one switch value for two flavours: the host's is named too.
|
||||
+ (flag('tso') === 'arm' && tso() ? '&tsohost=1' : '')
|
||||
// Named only when on: the cache of a page without it keeps its name.
|
||||
+ (globalThis.GEX_NULL_GUARD ? '&nullguard=1' : '');
|
||||
+ (globalThis.GEX_NULL_GUARD ? '&nullguard=1' : '')
|
||||
+ (globalThis.GEX_MXCSR ? '&mxcsr=1' : '');
|
||||
const flavour = globalThis.GEX_FLAVOUR ?? (typeof __GEX_FLAVOUR__ !== 'undefined' ? __GEX_FLAVOUR__ : '');
|
||||
return `xc-${xcHash(shimBytes, kernelBytes, switches, flavour, globalThis.location?.pathname ?? '')}`;
|
||||
}
|
||||
@@ -1106,6 +1107,8 @@ async function runProgramHere(
|
||||
w3Split: globalThis.GEX_W3_SPLIT || undefined,
|
||||
// `?nullguard=1`: the null guard, a debugging opt-in (gex_abi::null_guard).
|
||||
nullGuard: globalThis.GEX_NULL_GUARD || undefined,
|
||||
// `?mxcsr=1`: SSE under MXCSR, a per-program opt-in (gex_ir::mxcsr_exact).
|
||||
mxcsr: globalThis.GEX_MXCSR || undefined,
|
||||
// `?pinregion=BYTES`: an ejected project's libraries' region (gex_abi::set_pin_region).
|
||||
pinRegion: globalThis.GEX_PIN_REGION || undefined,
|
||||
// `?sehreport=1`: crash reports name the SEH chain (a debugging opt-in).
|
||||
|
||||
Reference in new issue
Block a user