Skip to content

Commit 460458b

Browse files
humanappCopilot
andauthored
Specialize ARM Thumb interface dispatch (microsoft#11339)
* Specialize thumb interface dispatch: skip arg-shuffling wrapper when safe, share dispatch thunks, fast-path string-map set * Commit copilot suggestion Remove unused method tryGetFieldInfo Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * remove unused conditional * Add codegen test corpus and native thumb test harness tests/thumb-test compiles programs to native ARM Thumb in-process against a captured micro:bit (mbcodal) CompileOptions fixture and asserts on the generated assembly listing; runs offline via gulp testthumb. lang-test0 gains three semantic files covering condition truthiness and lowering and interface dispatch (including both sides of the call-site-count thresholds that gate dispatch specialization), plus a deliberately disabled repro of the default-parameters-on-dynamic-dispatch defect and a coverage map (README-codegen.md) tying failure modes to tests and layers. * Add hw-ab: hardware A/B comparison tooling for micro:bit Cross-platform node scripts (macOS/Linux/Windows) to build candidate and reference hexes, flash a micro:bit, capture serial, and diff the traces, with a gcStats-based soak mode for leak detection. Driven by one wrapper: npm run hwab -- ab truthiness. * Harden hw-ab serial capture against stale and cross-case output Findings from running the capture against a real micro:bit: - DAPLink buffers serial while no host is reading, so a fresh capture can open onto the previously flashed program's output, including its PASS banner. Verdicts are now read only from the flashed program's own HWAB START banner onward, and --expect <case> (passed by hwab) requires the banner and verdict to name the flashed case. Timeout diagnostics report ignored foreign output and point at FAIL.TXT when no banner appears. - Panic does not flush the serial FIFO, which truncated the assert id on the wire. The generated device assert now drains for 250 ms between logging the id and panicking. * add 521 flash error guidance * harden hw-ab capture and diff against flash failures and serial artifacts * add checked-field-load baseline case * Add noIfaceSpec switch and differential codegen tests - gate the five dispatch specializations behind a noIfaceSpec compile switch; with it set, output matches the pre-specialization codegen and no helper text is emitted - fix the typed index-signature store specialization leaking into the JS backend, which called a runtime entry that only exists on thumb (broke two language tests, one pre-existing) - emit the index-signature store helper only when a program uses it, instead of in every build - flip the ifacebaseline expectations to the specialized codegen; add a noIfaceSpec variant pinning the codegen the switch restores - verified: testlang 57, testthumb 7, A/B hexes differ under the switch * flip fieldbaseline to the specialized codegen - flip the fieldbaseline expectations to the specialized codegen; add a noIfaceSpec variant pinning the codegen the switch restores - verified: testthumb 9, testlang 57, floors fault-injected --------- Co-authored-by: Eric Anderson <humanapp@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 1eb6af3 commit 460458b

6 files changed

Lines changed: 499 additions & 85 deletions

File tree

‎localtypings/pxtarget.d.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,7 @@ declare namespace ts.pxtc {
801801
rawELF?: boolean;
802802
multiVariant?: boolean;
803803
size?: boolean;
804+
noIfaceSpec?: boolean; // disable interface dispatch specializations
804805
noBoolLower?: boolean; // disable boolean condition lowering optimizations
805806
}
806807

‎pxtcompiler/emitter/backbase.ts‎

Lines changed: 207 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,11 @@ ${baseLabel}_nochk:
576576

577577
private emitFieldAccess(e: ir.Expr, store = false) {
578578
let info = e.data as FieldAccessInfo
579+
if (!store && this.shouldSpecializeCheckedFieldLoad(info)) {
580+
this.emitCheckedFieldLoad(info)
581+
return
582+
}
583+
579584
let pref = store ? "st" : "ld"
580585
let lbl = pref + "fld_" + info.classInfo.id + "_" + info.name
581586
if (info.needsCheck && !target.switches.skipClassCheck) {
@@ -596,6 +601,35 @@ ${baseLabel}_nochk:
596601
return
597602
}
598603

604+
private checkedFieldAccessCountKey(info: FieldAccessInfo) {
605+
return `${info.classInfo.id}:${info.name}:get`
606+
}
607+
608+
private shouldSpecializeCheckedFieldLoad(info: FieldAccessInfo) {
609+
if (!isThumb() || !this.bin.finalPass || target.switches.noIfaceSpec)
610+
return false
611+
if (target.switches.skipClassCheck || !info.needsCheck)
612+
return false
613+
return (this.bin.checkedFieldAccessCounts[this.checkedFieldAccessCountKey(info)] || 0) >= 5
614+
}
615+
616+
private emitCheckedFieldLoad(info: FieldAccessInfo) {
617+
const helperKey = "ldfldchk_" + info.classInfo.id + "_" + info.name
618+
const helper = this.ensureLabelledHelper(helperKey, () => {
619+
this.write(this.t.helper_prologue())
620+
this.emitInstanceOf(info.classInfo, "validate")
621+
let off = info.idx * 4 + 4
622+
let xoff = "#" + off
623+
if (off > 124) {
624+
this.write(this.t.emit_int(off, "r3"))
625+
xoff = "r3"
626+
}
627+
this.write(`ldr r0, [r0, ${xoff}]`)
628+
this.write(this.t.helper_epilogue())
629+
})
630+
this.write(this.t.call_lbl(helper, false, this.stackAlignmentNeeded(0)))
631+
}
632+
599633
private writeFailBranch() {
600634
this.write(`.fail:`)
601635
this.write(`mov r1, lr`)
@@ -839,13 +873,52 @@ ${baseLabel}_nochk:
839873

840874
private emitIfaceCall(procid: ir.ProcId, numargs: number, getset = "") {
841875
U.assert(procid.ifaceIndex > 0)
842-
this.write(this.t.emit_int(procid.ifaceIndex, "r1"))
843-
844-
this.emitLabelledHelper("ifacecall" + numargs + "_" + getset, () => {
876+
const helperKey = "ifacecall" + numargs + "_" + getset
877+
const helper = this.ensureLabelledHelper(helperKey, () => {
845878
this.write(`ldr r0, [sp, #0] ; ld-this`)
846879
this.loadVTable()
847880
this.ifaceCallCore(numargs, getset)
848881
})
882+
883+
if (this.shouldSpecializeIfaceCall(procid.ifaceIndex, numargs, getset)) {
884+
const thunkKey = helperKey + "_i" + procid.ifaceIndex
885+
this.emitLabelledHelper(thunkKey, () => {
886+
this.write(this.t.emit_int(procid.ifaceIndex, "r1"))
887+
this.write(`ldlit r7, ${helper}@fn`)
888+
this.write(`bx r7`)
889+
})
890+
} else {
891+
this.write(this.t.emit_int(procid.ifaceIndex, "r1"))
892+
this.write(this.t.call_lbl(helper))
893+
}
894+
}
895+
896+
private ifaceCallCountKey(ifaceIndex: number, numargs: number, getset: string) {
897+
return `${ifaceIndex}:${numargs}:${getset || "call"}`
898+
}
899+
900+
// Count-driven specialization decisions (this method and the related
901+
// shouldSpecializeCheckedFieldLoad / shouldSpecializeMapSetByFieldId) assume:
902+
// - bin.finalPass is true, AND
903+
// - the count maps were populated during the same final-pass emit()
904+
// call that builds the IR this asm walk consumes.
905+
// This is currently true because the frontend populates counts during IR
906+
// generation and the backend reads them only during the subsequent asm walk.
907+
// Any future change that interleaves IR generation with asm emission would
908+
// silently regress these to "always false".
909+
//
910+
// Threshold of 3 is the break-even point for the labelled-thunk pattern:
911+
// each shared call site saves ~4 bytes vs an inline call, and the thunk
912+
// itself costs ~12 bytes. At 3 callers, savings break even with thunk cost.
913+
private shouldSpecializeIfaceCall(ifaceIndex: number, numargs: number, getset: string) {
914+
if (!isThumb() || !this.bin.finalPass || target.switches.noIfaceSpec)
915+
return false
916+
const count = this.bin.ifaceCallCounts[this.ifaceCallCountKey(ifaceIndex, numargs, getset)] || 0
917+
if (getset == "get" && numargs == 1)
918+
return count >= 3
919+
if (getset == "set" || !getset)
920+
return count >= 3
921+
return false
849922
}
850923

851924
// vtable in r3; clobber r2
@@ -951,6 +1024,8 @@ ${baseLabel}_nochk:
9511024

9521025
private emitRtCall(topExpr: ir.Expr, genCall: () => void = null) {
9531026
let name: string = topExpr.data
1027+
const mapSetFieldId = this.mapSetByFieldId(topExpr)
1028+
const specializeMapSetFieldId = mapSetFieldId !== undefined && this.shouldSpecializeMapSetByFieldId(mapSetFieldId)
9541029

9551030
if (name == "pxt::beginTry") {
9561031
return this.emitBeginTry(topExpr)
@@ -965,6 +1040,8 @@ ${baseLabel}_nochk:
9651040
isRef: (maskInfo.refMask & (1 << i)) != 0,
9661041
conv: convs.find(c => c.argIdx == i)
9671042
}))
1043+
if (specializeMapSetFieldId)
1044+
allArgs = allArgs.filter(a => a.idx != 1)
9681045

9691046
U.assert(allArgs.length <= 4)
9701047

@@ -1071,7 +1148,9 @@ ${baseLabel}_nochk:
10711148
if (genCall) {
10721149
genCall()
10731150
} else {
1074-
if (name != "langsupp::ignore")
1151+
if (specializeMapSetFieldId)
1152+
this.emitMapSetByFieldIdCall(mapSetFieldId)
1153+
else if (name != "langsupp::ignore")
10751154
this.alignedCall(name, "", 0, true)
10761155
}
10771156

@@ -1081,27 +1160,67 @@ ${baseLabel}_nochk:
10811160
}
10821161
}
10831162

1163+
private mapSetByFieldId(topExpr: ir.Expr) {
1164+
if (topExpr.data != "pxtrt::mapSet" || topExpr.args.length != 3)
1165+
return undefined
1166+
const fieldId = topExpr.args[1]
1167+
if (fieldId.exprKind != ir.EK.NumberLiteral || typeof fieldId.data != "number")
1168+
return undefined
1169+
return fieldId.data as number
1170+
}
1171+
1172+
private shouldSpecializeMapSetByFieldId(fieldId: number) {
1173+
if (!isThumb() || !this.bin.finalPass || target.switches.noIfaceSpec)
1174+
return false
1175+
return (this.bin.mapSetByFieldIdCounts[fieldId + ""] || 0) >= 3
1176+
}
1177+
1178+
private emitMapSetByFieldIdCall(fieldId: number) {
1179+
const helper = this.ensureLabelledHelper("mapset_i" + fieldId, () => {
1180+
this.write(this.t.helper_prologue())
1181+
this.write(this.t.emit_int(fieldId, "r1"))
1182+
this.write(this.t.callCPP("pxtrt::mapSet"))
1183+
this.write(this.t.helper_epilogue())
1184+
})
1185+
this.write(this.t.call_lbl(helper, false, this.stackAlignmentNeeded(0)))
1186+
}
1187+
10841188
private alignedCall(name: string, cmt = "", off = 0, saveStack = false) {
10851189
if (U.startsWith(name, "_cmp_") || U.startsWith(name, "_pxt_"))
10861190
saveStack = false
10871191
this.write(this.t.call_lbl(name, saveStack, this.stackAlignmentNeeded(off)) + cmt)
10881192
}
10891193

1090-
private emitLabelledHelper(lbl: string, generate: () => void) {
1194+
// Lazily emit a deduped helper body and return its label.
1195+
// Use this when you want to BRANCH to (or reference) the helper without
1196+
// emitting a `bl` at the current cursor -- e.g. when wrapping the call in
1197+
// a thunk, or storing the label for later use.
1198+
private ensureLabelledHelper(lbl: string, generate: () => void) {
10911199
if (!this.labelledHelpers[lbl]) {
10921200
let outp = this.redirectOutput(generate)
1093-
this.emitHelper(outp, lbl)
1094-
this.labelledHelpers[lbl] = this.bin.codeHelpers[outp];
1095-
} else {
1096-
this.write(this.t.call_lbl(this.labelledHelpers[lbl]))
1201+
this.labelledHelpers[lbl] = this.helperLabel(outp, lbl);
10971202
}
1203+
return this.labelledHelpers[lbl]
10981204
}
10991205

1100-
private emitHelper(asm: string, baseName = "hlp") {
1206+
// Ensure a deduped helper exists and emit a `bl` to it at the cursor.
1207+
// Use this for the common case of "factor this sequence into a helper and call it here".
1208+
private emitLabelledHelper(lbl: string, generate: () => void) {
1209+
const helper = this.ensureLabelledHelper(lbl, generate)
1210+
this.write(this.t.call_lbl(helper))
1211+
return helper
1212+
}
1213+
1214+
private helperLabel(asm: string, baseName = "hlp") {
11011215
if (!this.bin.codeHelpers[asm]) {
11021216
let len = Object.keys(this.bin.codeHelpers).length
11031217
this.bin.codeHelpers[asm] = `_${baseName}_${len}`
11041218
}
1219+
return this.bin.codeHelpers[asm]
1220+
}
1221+
1222+
private emitHelper(asm: string, baseName = "hlp") {
1223+
this.helperLabel(asm, baseName)
11051224
this.write(this.t.call_lbl(this.bin.codeHelpers[asm]))
11061225
}
11071226

@@ -1144,43 +1263,42 @@ ${baseLabel}_nochk:
11441263
}
11451264
}
11461265

1147-
private emitFieldMethods() {
1148-
for (let op of ["get", "set"]) {
1149-
this.write(`
1266+
private emitFieldMethod(op: string) {
1267+
this.write(`
11501268
${this.helperObject(op)}
11511269
.section code
11521270
_pxt_map_${op}:
11531271
`)
11541272

1155-
this.loadVTable("r4")
1156-
this.checkSubtype(this.builtInClassNo(pxt.BuiltInType.RefMap), ".notmap", "r4")
1273+
this.loadVTable("r4")
1274+
this.checkSubtype(this.builtInClassNo(pxt.BuiltInType.RefMap), ".notmap", "r4")
11571275

1158-
this.write(this.t.callCPPPush(op == "set" ? "pxtrt::mapSetByString" : "pxtrt::mapGetByString"))
1159-
this.write(".notmap:")
1276+
this.write(this.t.callCPPPush(op == "set" ? "pxtrt::mapSetByString" : "pxtrt::mapGetByString"))
1277+
this.write(".notmap:")
11601278

1161-
let numargs = op == "set" ? 2 : 1
1162-
let hasAlign = false
1279+
let numargs = op == "set" ? 2 : 1
1280+
let hasAlign = false
11631281

1164-
this.write("mov r4, r3 ; save VT")
1282+
this.write("mov r4, r3 ; save VT")
11651283

1166-
if (op == "set") {
1167-
if (target.stackAlign) {
1168-
hasAlign = true
1169-
this.write("push {lr} ; align")
1170-
}
1171-
this.write(`
1284+
if (op == "set") {
1285+
if (target.stackAlign) {
1286+
hasAlign = true
1287+
this.write("push {lr} ; align")
1288+
}
1289+
this.write(`
11721290
push {r0, r2, lr}
11731291
mov r0, r1
11741292
`)
11751293

1176-
} else {
1177-
this.write(`
1294+
} else {
1295+
this.write(`
11781296
push {r0, lr}
11791297
mov r0, r1
11801298
`)
1181-
}
1299+
}
11821300

1183-
this.write(`
1301+
this.write(`
11841302
bl pxtrt::lookupMapKey
11851303
mov r1, r0 ; put key index in r1
11861304
ldr r0, [sp, #0] ; restore obj pointer
@@ -1189,13 +1307,53 @@ ${baseLabel}_nochk:
11891307
`)
11901308

11911309

1192-
this.write(this.t.pop_locals(numargs + (hasAlign ? 1 : 0)))
1310+
this.write(this.t.pop_locals(numargs + (hasAlign ? 1 : 0)))
11931311

1194-
this.write("pop {pc}")
1312+
this.write("pop {pc}")
11951313

1196-
this.write(".dowork:")
1197-
this.ifaceCallCore(numargs, op, true)
1198-
}
1314+
this.write(".dowork:")
1315+
this.ifaceCallCore(numargs, op, true)
1316+
}
1317+
1318+
private emitStringMapSetMethod() {
1319+
// Fast path for the well-typed "map with string index signature" case.
1320+
// - tagged-int / null receivers: unrecoverable, panic with failedCast (matches the
1321+
// pre-optimization mapSetGeneric path, which also can't proceed on these).
1322+
// - non-RefMap pointer receivers: tail-call _pxt_map_set, which handles
1323+
// non-map objects via interface dispatch. Preserves back-compat for code that
1324+
// casts non-map objects to a string-index-signature type.
1325+
//
1326+
// Emitted only when the final-pass IR selected mapSetByStringOnly
1327+
// (never under `noIfaceSpec`): unreferenced helper text costs image
1328+
// space in every program otherwise.
1329+
if (!this.bin.usesMapSetByStringOnly)
1330+
return
1331+
this.write(`
1332+
${this.helperObject("set string map")}
1333+
.section code
1334+
_pxt_map_set_by_string:
1335+
lsls r4, r0, #30
1336+
bne .badtype
1337+
cmp r0, #0
1338+
beq .badtype
1339+
ldr r3, [r0, #0]
1340+
`)
1341+
1342+
this.write(`
1343+
ldrh r4, [r3, #8]
1344+
cmp r4, #${pxt.BuiltInType.RefMap}
1345+
bne .fallback
1346+
`)
1347+
this.write(this.t.callCPPPush("pxtrt::mapSetByString"))
1348+
this.write(`
1349+
.fallback:
1350+
b _pxt_map_set
1351+
.badtype:
1352+
mov r1, lr
1353+
mov r7, sp
1354+
str r7, [r6, #4]
1355+
bl pxt::failedCast
1356+
`)
11991357
}
12001358

12011359
private emitArrayMethod(op: string, isBuffer: boolean) {
@@ -1292,6 +1450,13 @@ ${baseLabel}_nochk:
12921450
}
12931451
}
12941452

1453+
private emitFieldMethods() {
1454+
for (let op of ["get", "set"]) {
1455+
this.emitFieldMethod(op)
1456+
}
1457+
this.emitStringMapSetMethod()
1458+
}
1459+
12951460
private emitLambdaTrampoline() {
12961461
let r3 = target.stackAlign ? "r3," : ""
12971462
this.write(`
@@ -1659,6 +1824,13 @@ ${baseLabel}_nochk:
16591824
this.write(`.word ${this.proc.label()}_args@fn`)
16601825
}
16611826

1827+
if (this.proc.useExactIfaceWrapper) {
1828+
this.write(`${this.proc.label()}_iface:`)
1829+
this.write(`b ${this.proc.label()}_nochk`)
1830+
if (!this.proc.info.usedAsValue)
1831+
return
1832+
}
1833+
16621834
this.write(`${this.proc.label()}_args:`)
16631835

16641836
let numargs = this.proc.args.length

0 commit comments

Comments
 (0)