Skip to content

Commit ca2bc7e

Browse files
committed
Read shared node members through per-kind offset tables
Members shared by many kinds, such as Expression, Name, Type and Modifiers, were read through generated switches over the kind. Those switches are too large to inline, and profiles of the checker showed them among the most expensive functions in the ast package, which as a whole took nearly twice the share of cycles that it does on main. Every kind with such a member stores it at a fixed offset in its record, so a table indexed by kind gives the offset, and reading the member becomes a load from the table and a load from the record. The kind tests behind DeclarationData and similar accessors become boolean tables for the same reason. │ old.txt │ new.txt │ │ check-sec │ check-sec vs base │ VSCode 7.369 ± 1% 7.229 ± 1% -1.89% (p=0.003 n=8) │ old.txt │ new.txt │ │ total-sec │ total-sec vs base │ VSCode 8.183 ± 1% 8.036 ± 1% -1.79% (p=0.001 n=8) │ old.txt │ new.txt │ │ mem-B │ mem-B vs base │ VSCode 3.208Gi ± 0% 3.207Gi ± 0% ~ (p=0.234 n=8)
1 parent 4b85ba3 commit ca2bc7e

5 files changed

Lines changed: 557 additions & 1056 deletions

File tree

‎tools/scripts/tsc/generate-go-ast.ts‎

Lines changed: 69 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,81 +1228,98 @@ function transitiveBaseKeys(node: NodeType): Set<string> {
12281228
return seen;
12291229
}
12301230

1231-
function generateNodeAccessorDispatch(
1232-
w: CodeWriter,
1233-
method: string,
1234-
ret: string,
1235-
hasAccessor: (node: NodeType) => boolean,
1236-
body: (node: NodeType) => string,
1237-
zero: string,
1238-
) {
1239-
const cases: { node: NodeType; kinds: string[]; }[] = [];
1240-
for (const node of api.nodes()) {
1241-
if (!hasAccessor(node)) continue;
1242-
const kinds = node.allKinds().map(kind => kind.formatGoConstant());
1243-
if (kinds.length !== 0) cases.push({ node, kinds });
1244-
}
1245-
1246-
// Large sparse Kind switches become comparison trees. Dense indices let Go
1247-
// emit jump tables; zero is reserved for kinds without the accessor.
1248-
const useDispatchTable = cases.length >= 8;
1249-
const table = `node${method}Dispatch`;
1250-
if (useDispatchTable) {
1251-
if (cases.length > 255) {
1252-
throw new Error(`${method} dispatch exceeds the uint8 index range`);
1253-
}
1254-
w.write(`var ${table} = [kindFlowReduceLabelData + 1]uint8{`);
1231+
// Kinds have a base when their records hold its fields, so testing for it is
1232+
// a table lookup.
1233+
function generateNodeAccessors(w: CodeWriter) {
1234+
for (const { method, ret, base } of NODE_ACCESSORS) {
1235+
const table = `kindHas${base}`;
1236+
w.write(`var ${table} = [kindFlowReduceLabelData + 1]bool{`);
12551237
w.push();
1256-
for (const [index, { kinds }] of cases.entries()) {
1257-
for (const kind of kinds) {
1258-
w.write(`${kind}: ${index + 1},`);
1259-
}
1238+
for (const node of api.nodes()) {
1239+
if (!transitiveBaseKeys(node).has(base)) continue;
1240+
for (const kind of node.allKinds()) w.write(`${kind.formatGoConstant()}: true,`);
12601241
}
12611242
w.pop();
12621243
w.write("}");
12631244
w.write("");
1245+
w.write(`func (n Node) ${method}() ${ret} {`);
1246+
w.push();
1247+
w.write(`if ${table}[n.Kind()] {`);
1248+
w.push();
1249+
w.write(`return ${ret}{n}`);
1250+
w.pop();
1251+
w.write("}");
1252+
w.write(`return ${ret}{}`);
1253+
w.pop();
1254+
w.write("}");
1255+
w.write("");
12641256
}
1257+
}
12651258

1266-
w.write(`func (n Node) ${method}() ${ret} {`);
1259+
function hasMember(node: NodeType, name: string): boolean {
1260+
return schemaMembers(node).some(member => member.name === name);
1261+
}
1262+
1263+
function generateMemberOffsetTable(w: CodeWriter, table: string, member: string, refType: string) {
1264+
const storage = goStorageName(member);
1265+
w.write(`var ${table} = [kindFlowReduceLabelData + 1]uint8{`);
12671266
w.push();
1268-
if (useDispatchTable) {
1269-
w.write(`switch ${table}[n.Kind()] {`);
1270-
}
1271-
else {
1272-
w.write("switch n.Kind() {");
1273-
}
1274-
for (const [index, { node, kinds }] of cases.entries()) {
1275-
w.write(`case ${useDispatchTable ? index + 1 : kinds.join(", ")}:`);
1276-
w.push();
1277-
w.write(`return ${body(node)}`);
1278-
w.pop();
1267+
for (const node of api.nodes()) {
1268+
const field = nodeRecordFields(node).find(f => f.storage === storage);
1269+
// Only refs can be read the same way for every kind.
1270+
if (field?.goType !== refType) continue;
1271+
for (const kind of node.allKinds()) {
1272+
w.write(`${kind.formatGoConstant()}: uint8(unsafe.Offsetof(${dataName(node)}{}.${storage})),`);
1273+
}
12791274
}
1280-
w.write("default:");
1275+
w.pop();
1276+
w.write("}");
1277+
w.write("");
1278+
}
1279+
1280+
// A member that every node having it stores as the same kind of ref is read
1281+
// through a table of its offsets by kind, zero for kinds without it.
1282+
function generateMemberOffsetAccessor(w: CodeWriter, method: string, ret: string, member: string, refType: string, deref: string, zero: string) {
1283+
const table = `node${method}Offsets`;
1284+
generateMemberOffsetTable(w, table, member, refType);
1285+
w.write(`func (n Node) ${method}() ${ret} {`);
12811286
w.push();
1282-
w.write(`return ${zero}`);
1287+
w.write(`if off := ${table}[n.Kind()]; off != 0 {`);
1288+
w.push();
1289+
w.write(`return ${deref}((*${refType})(unsafe.Add(n.rec(), off)))`);
12831290
w.pop();
12841291
w.write("}");
1292+
w.write(`return ${zero}`);
12851293
w.pop();
12861294
w.write("}");
12871295
w.write("");
12881296
}
12891297

1290-
function generateNodeAccessors(w: CodeWriter) {
1291-
for (const { method, ret, base } of NODE_ACCESSORS) {
1292-
generateNodeAccessorDispatch(w, method, ret, node => transitiveBaseKeys(node).has(base), () => `${ret}{n}`, `${ret}{}`);
1293-
}
1294-
}
1298+
// Members read through Node by hand-written accessors, which find them by
1299+
// these tables.
1300+
const MEMBER_OFFSET_TABLES: { member: string; refType: string; }[] = [
1301+
{ member: "Expression", refType: "nodeRef" },
1302+
{ member: "Initializer", refType: "nodeRef" },
1303+
{ member: "Type", refType: "nodeRef" },
1304+
{ member: "PostfixToken", refType: "nodeRef" },
1305+
{ member: "QuestionToken", refType: "nodeRef" },
1306+
{ member: "QuestionDotToken", refType: "nodeRef" },
1307+
{ member: "TypeArguments", refType: "listRef" },
1308+
{ member: "Arguments", refType: "listRef" },
1309+
];
12951310

1296-
function hasMember(node: NodeType, name: string): boolean {
1297-
return schemaMembers(node).some(member => member.name === name);
1311+
function generateMemberOffsetTables(w: CodeWriter) {
1312+
for (const { member, refType } of MEMBER_OFFSET_TABLES) {
1313+
generateMemberOffsetTable(w, `node${member}Offsets`, member, refType);
1314+
}
12981315
}
12991316

13001317
function generateNameDispatch(w: CodeWriter) {
1301-
generateNodeAccessorDispatch(w, "Name", "DeclarationName", node => hasMember(node, "name"), node => `${viewOf(node, "n")}.Name()`, "Node{}");
1318+
generateMemberOffsetAccessor(w, "Name", "DeclarationName", "name", "nodeRef", "n.deref", "Node{}");
13021319
}
13031320

13041321
function generateModifiersDispatch(w: CodeWriter) {
1305-
generateNodeAccessorDispatch(w, "Modifiers", "ModifierList", node => hasMember(node, "modifiers"), node => `${viewOf(node, "n")}.Modifiers()`, "ModifierList{}");
1322+
generateMemberOffsetAccessor(w, "Modifiers", "ModifierList", "modifiers", "listRef", "n.derefModifiers", "ModifierList{}");
13061323
}
13071324

13081325
function generateSetModifiersDispatch(w: CodeWriter) {
@@ -1695,6 +1712,7 @@ function generate(): string {
16951712
w.write("");
16961713
generateNameDispatch(w);
16971714
generateModifiersDispatch(w);
1715+
generateMemberOffsetTables(w);
16981716
generateSetModifiersDispatch(w);
16991717

17001718
// As*() casts

0 commit comments

Comments
 (0)