From fa046b8757ba46eded66e14d0999ea52149043a4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 27 Oct 2026 17:32:28 -0500 Subject: [PATCH] Compile dev executables of more than 64 KiB in partitions A dev executable was one partition up to 1 MiB of LLVM text, so every edit compiled the whole program again. Unoptimized partitions are compiled on their own, with nothing imported across them, so dividing a program does not change its code's quality, and the helper reuses each partition an edit does not change from the partition cache. nativeProgramPartitions now takes whether the program is optimized. Dev executables are divided from 64 KiB of LLVM text, with at least 32 KiB per partition: four partitions up to 128 KiB, then at least 8 as before. Release and speed executables keep their partition and code generation counts. --- docs/content/docs/how-it-works.mdx | 2 +- .../src/backend/native-codegen-core.ts | 24 ++++++++++++++----- .../src/backend/native-codegen.test.ts | 8 +++++++ packages/compiler/src/index.ts | 4 +++- packages/compiler/src/native/compiler.ts | 4 +++- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/docs/content/docs/how-it-works.mdx b/docs/content/docs/how-it-works.mdx index 0475e311e..8196460cf 100644 --- a/docs/content/docs/how-it-works.mdx +++ b/docs/content/docs/how-it-works.mdx @@ -42,7 +42,7 @@ Parsing, type checking, lowering, and LLVM IR generation run one after another o ### Partitions -The LLVM helper divides an executable's LLVM IR into partitions by its size. Up to 1 MiB of LLVM text is one partition, and up to 2 MiB is four. Larger programs get one partition per 4 MiB of LLVM text, rounded up to a power of two, and at least 8 and at most 64. `--emit=llvm` writes this text, so its file size shows how many partitions a program gets. +The LLVM helper divides an executable's LLVM IR into partitions by its size. Up to 1 MiB of LLVM text is one partition, and up to 2 MiB is four. Larger programs get one partition per 4 MiB of LLVM text, rounded up to a power of two, and at least 8 and at most 64. `--emit=llvm` writes this text, so its file size shows how many partitions a program gets. `dev` executables, whose partitions are compiled on their own with nothing imported, are divided from 64 KiB: up to 128 KiB is four partitions, and larger `dev` programs get at least 8. diff --git a/packages/compiler/src/backend/native-codegen-core.ts b/packages/compiler/src/backend/native-codegen-core.ts index d75dc2097..e1e726ef4 100644 --- a/packages/compiler/src/backend/native-codegen-core.ts +++ b/packages/compiler/src/backend/native-codegen-core.ts @@ -14,20 +14,32 @@ const PROGRAM_PARTITION_BYTES = 4 * 1024 * 1024; const MIN_PARTITION_BYTES = 512 * 1024; const MIN_PROGRAM_PARTITIONS = 8; const MAX_PROGRAM_PARTITIONS = 64; +/** SINGLE_PARTITION_BYTES and MIN_PARTITION_BYTES of unoptimized (dev) + * executables. */ +const UNOPTIMIZED_SINGLE_PARTITION_BYTES = 64 * 1024; +const UNOPTIMIZED_MIN_PARTITION_BYTES = 32 * 1024; /** Executable objects larger than SINGLE_PARTITION_BYTES are divided into * partitions that the helper simplifies, optimizes and compiles concurrently, * importing small functions across partitions for inlining, and reuses * individually across builds. A divided module gets at least * MIN_PROGRAM_PARTITIONS so that mid-size programs use several cores. The - * count is a power of two that depends only on the module's size, on every - * target, so artifacts never vary with the host and ordinary edits keep every - * function in its partition. */ -export function nativeProgramPartitions(llvmBytes: number): number { - if (llvmBytes <= SINGLE_PARTITION_BYTES) return 1; + * count is a power of two that depends only on the module's size and + * whether it is optimized, on every target, so artifacts never vary with the + * host and ordinary edits keep every function in its partition. + * + * Unoptimized (dev) partitions are compiled on their own with nothing + * imported, so they cost no run time and an edit compiles only the partitions + * whose functions it changes: dev executables are divided from + * UNOPTIMIZED_SINGLE_PARTITION_BYTES, at least + * UNOPTIMIZED_MIN_PARTITION_BYTES each. */ +export function nativeProgramPartitions(llvmBytes: number, unoptimized = false): number { + const single = unoptimized ? UNOPTIMIZED_SINGLE_PARTITION_BYTES : SINGLE_PARTITION_BYTES; + const least = unoptimized ? UNOPTIMIZED_MIN_PARTITION_BYTES : MIN_PARTITION_BYTES; + if (llvmBytes <= single) return 1; const required = Math.max( Math.ceil(llvmBytes / PROGRAM_PARTITION_BYTES), - Math.min(MIN_PROGRAM_PARTITIONS, Math.ceil(llvmBytes / MIN_PARTITION_BYTES)), + Math.min(MIN_PROGRAM_PARTITIONS, Math.ceil(llvmBytes / least)), ); return Math.min(MAX_PROGRAM_PARTITIONS, 2 ** Math.ceil(Math.log2(required))); } diff --git a/packages/compiler/src/backend/native-codegen.test.ts b/packages/compiler/src/backend/native-codegen.test.ts index ff0224746..fdee2e5ca 100644 --- a/packages/compiler/src/backend/native-codegen.test.ts +++ b/packages/compiler/src/backend/native-codegen.test.ts @@ -408,6 +408,14 @@ test("program partitions depend only on module size", () => { expect(nativeProgramPartitions(32 * megabyte + 1)).toBe(16); expect(nativeProgramPartitions(100 * megabyte)).toBe(32); expect(nativeProgramPartitions(1024 * megabyte)).toBe(64); + // Unoptimized (dev) programs are divided from 64 KiB. + const kilobyte = 1024; + expect(nativeProgramPartitions(64 * kilobyte, true)).toBe(1); + expect(nativeProgramPartitions(64 * kilobyte + 1, true)).toBe(4); + expect(nativeProgramPartitions(128 * kilobyte, true)).toBe(4); + expect(nativeProgramPartitions(128 * kilobyte + 1, true)).toBe(8); + expect(nativeProgramPartitions(megabyte, true)).toBe(8); + expect(nativeProgramPartitions(32 * megabyte + 1, true)).toBe(16); expect(nativePartitionPaths("/build.d/program.o", 3)).toEqual([ "/build.d/program.o", "/build.d/program.part1.o", diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index b002e2437..c41653565 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -784,7 +784,9 @@ async function emitNativeProgramObject( // An optimized program that stays one partition is optimized whole and // generates code for its parts concurrently. const codegenParts = single || opts.optimization === "dev" ? 1 : nativeCodegenParts(llvmBytes); - const partitions = single ? 1 : Math.max(nativeProgramPartitions(llvmBytes), codegenParts); + const partitions = single + ? 1 + : Math.max(nativeProgramPartitions(llvmBytes, opts.optimization === "dev"), codegenParts); try { const artifact = await emitNativeArtifact({ outputPath: linkPath, diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index 19351a81a..fafae4f9d 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -386,7 +386,9 @@ export class NativeCompiler { // An optimized program that stays one partition is optimized whole and // generates code for its parts concurrently. const codegenParts = library || optimization === "dev" ? 1 : nativeCodegenParts(size); - const partitions = library ? 1 : Math.max(nativeProgramPartitions(size), codegenParts); + const partitions = library + ? 1 + : Math.max(nativeProgramPartitions(size, optimization === "dev"), codegenParts); return this.emitObject( input, output,