From 797b41751e62ca6264844d071eb6d04636fa8525 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Wed, 14 Oct 2026 12:11:00 +0800 Subject: [PATCH] =?UTF-8?q?perf(=E6=8F=90=E4=BA=A4):=20=E4=BA=A4=20C=20/?= =?UTF-8?q?=20C++=20=E4=B8=8D=E7=94=A8=E5=86=8D=E5=B9=B2=E7=AD=89=EF=BC=9A?= =?UTF-8?q?=E7=BC=96=E5=BE=97=E8=BF=87=E7=9A=84=E4=BB=A3=E7=A0=81=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=85=88=E9=80=81=E5=88=A4=E9=A2=98=E6=9C=BA=E7=BC=96?= =?UTF-8?q?=E4=B8=80=E9=81=8D=EF=BC=8C=E5=85=A8=E7=8F=AD=E4=B8=80=E8=B5=B7?= =?UTF-8?q?=E4=BA=A4=E6=97=B6=E4=B8=8D=E4=BC=9A=E5=8D=A1=E4=B8=8A=E5=A5=BD?= =?UTF-8?q?=E5=87=A0=E7=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 提交前的自动格式化原来每次都先在判题机上编一次(防止 clang-format 把编不过的几行并成 一行)。服务器只有两核、还要和判题抢 CPU,按两核实测:40 人同时交 C,这一步中位 1.8 秒、 最慢 2.7 秒;40 人同时交 C++(iostream),全部撞上 8 秒超时。而且等完之后判题还要再编一遍。 现在先跑 clang-format(十几毫秒),只有它真把学生原来分开的两行并成一行时才去判题机编。 大括号挪到上一行、} else 接上不算并行。拿本机库里的真实提交核过:编得过的 C 只有 1.2%、 C++ 2.1% 还会去编;编不过又被并了行的照样拦住、原样返回。 顺带:格式化不再给 #include 排序、不再把超长的 printf 字符串拆成两截(学生看不懂 "……" "……" 这种写法,它们也是误判的主要来源)。 Co-Authored-By: Claude Opus 5.5 --- apps/api/src/routes/submission.ts | 42 +++++++++++++++------------- apps/api/src/services/format-code.ts | 39 +++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/apps/api/src/routes/submission.ts b/apps/api/src/routes/submission.ts index 0afd86a..8c78d14 100644 --- a/apps/api/src/routes/submission.ts +++ b/apps/api/src/routes/submission.ts @@ -28,7 +28,7 @@ import { requireContestAccess, type ContestEnv, } from "../services/contest" -import { CodeFormatError, formatCode } from "../services/format-code" +import { CodeFormatError, formatCode, mergesLines } from "../services/format-code" import { getBooleanOption } from "../services/options" import { problemSetLockCutoffs } from "../services/problemset" import { consumeToken } from "../services/throttling" @@ -246,28 +246,30 @@ submissionRoutes.post("/submissions/:id/rejudge", requireTeacher, async (c) => { submissionRoutes.post("/code/format", requireAuth, async (c) => { const parsed = await parseBody(c, formatCodeRequestSchema, "Invalid format payload") if (!parsed.success) return parsed.response - // C / C++ 编不过就原样返回,不格式化(理由见 judge/run.ts 的 compileFails)。 - // 代码照常提交、照常判成编译错误,只是不改动学生的代码 const { code: source, language, problemId } = parsed.data - if (language === "c" || language === "cpp") { - const judgeLanguage = language === "c" ? "C" : "C++" - const [problem] = - problemId === undefined - ? [] - : await db - .select({ template: schema.problem.template }) - .from(schema.problem) - .where(eq(schema.problem.id, problemId)) - .limit(1) - const rawTemplate = problem ? templateForLanguage(problem.template, judgeLanguage) : null - const template = rawTemplate ? parseProblemTemplate(rawTemplate) : null - const full = template ? `${template.prepend}\n${source}\n${template.append}` : source - if (await compileFails(judgeLanguage, full)) { - return success(c, { code: source } satisfies FormatCodeResponse) - } - } try { const code = await formatCode(source, language) + // C / C++ 编不过就原样返回,不格式化(理由见 judge/run.ts 的 compileFails)。 + // 代码照常提交、照常判成编译错误,只是不改动学生的代码。 + // 只有 clang-format 真把两行并成一行时才去判题机编:判题机和判题抢同一台机器的 CPU, + // 全班一起交 C++ 时每人都要排满 8 秒超时;而编得过的代码九成以上根本不并行 + if ((language === "c" || language === "cpp") && mergesLines(source, code)) { + const judgeLanguage = language === "c" ? "C" : "C++" + const [problem] = + problemId === undefined + ? [] + : await db + .select({ template: schema.problem.template }) + .from(schema.problem) + .where(eq(schema.problem.id, problemId)) + .limit(1) + const rawTemplate = problem ? templateForLanguage(problem.template, judgeLanguage) : null + const template = rawTemplate ? parseProblemTemplate(rawTemplate) : null + const full = template ? `${template.prepend}\n${source}\n${template.append}` : source + if (await compileFails(judgeLanguage, full)) { + return success(c, { code: source } satisfies FormatCodeResponse) + } + } return success(c, { code } satisfies FormatCodeResponse) } catch (error) { if (error instanceof CodeFormatError) { diff --git a/apps/api/src/services/format-code.ts b/apps/api/src/services/format-code.ts index 5bf71c2..0d126f4 100644 --- a/apps/api/src/services/format-code.ts +++ b/apps/api/src/services/format-code.ts @@ -82,6 +82,41 @@ async function checkPythonSyntax(code: string): Promise() + for (const char of code) { + if (char === "\n") breaks.add(stream.length) + else if (!/\s/.test(char)) stream += char + } + breaks.delete(0) + breaks.delete(stream.length) + return { stream, breaks } +} + +/** + * clang-format 有没有把学生原来分开的两行并成一行。并了才值得怀疑代码编不过 + * (`int n;` 和下一行、少了分号的两行,见 judge/run.ts 的 compileFails)。 + * + * 风格本身就会并的两种不算:`{` 挪到上一行末尾(BreakBeforeBraces: Attach), + * `}` 和后面的 `else` / `while` 接上。别的差异(包括 clang-format 动了非空白字符, + * 比如排了 #include 的顺序)一律算可疑 —— 宁可多编一次,也不放过一次并行。 + */ +export function mergesLines(original: string, formatted: string) { + const before = lineBreaks(original) + const after = lineBreaks(formatted) + if (before.stream !== after.stream) return true + for (const at of before.breaks) { + if (after.breaks.has(at)) continue + const next = before.stream.slice(at) + if (next.startsWith("{")) continue + if (before.stream[at - 1] === "}" && /^(else|while)/.test(next)) continue + return true + } + return false +} + export async function formatCode(code: string, language: "python" | "c" | "cpp" | "sql") { if (language === "sql") return formatSql(code) @@ -101,11 +136,13 @@ export async function formatCode(code: string, language: "python" | "c" | "cpp" } const filename = language === "c" ? "code.c" : "code.cpp" + // 不排 #include、不拆长字符串:学生的 printf("……") 被拆成 "……" "……" 两截看不懂; + // 这两样也是 mergesLines 唯一常见的误报来源(它们改了非空白字符) const result = await runFormatter( [ config.clangFormatPath, `-assume-filename=${filename}`, - "-style={BasedOnStyle: LLVM, IndentWidth: 4, BreakBeforeBraces: Attach}", + "-style={BasedOnStyle: LLVM, IndentWidth: 4, BreakBeforeBraces: Attach, SortIncludes: Never, BreakStringLiterals: false}", ], code, )