From a0597455629618a282b582d13c23bfeab786e7d2 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Tue, 22 Sep 2026 10:46:16 +0800 Subject: perf(kernel): P27 第二步 — Rumor 扫描去装箱 + 裁剪判定 O(1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两条互相独立、均不改变保留集合与查询结果的优化: 1) 扫描去装箱:latestRumorFor / rumorIsDuplicate 用 `=` 比较 [] 的 NpcId/RumorId(含 RumorId option)会走泛型结构相等并逐条装箱(实测 ~24 B/条, Sim.chat 单独测得 N=16000 时 393,736 B/次、随 N 线性)。改为先解构底层 int/int64 再比较;Parent 用 int64 option 比较替代 RumorId option 结构相等。 语义完全等价,扫描不再逐条分配。 2) 裁剪判定 O(1):World 新增派生镜像 RumorCount/RumorOldestDay(不参与存档, 读档由列表经 rumorWorkingSetStats 重建)。appendRumor 用「count+1>capacity 或 min(oldest,day) List.fold (fun acc rumor -> min acc rumor.DayIndex) System.Int64.MaxValue) + + let private oldestRumorDay (rumors: RumorEvent list) : int64 = snd (rumorWorkingSetStats rumors) + + /// 追加一条谣言并维护派生元数据。容量内且新鲜时等价于 `rumor :: rumors`,且**不再全表扫描** + /// 判断是否需要裁剪(旧 `consRumor` 每次追加都会 `rumorsNeedTrim` 走一遍工作集); + /// 仅当条数超容量或存在早于保留窗口的条目时才走 `trimRumors`,保留集合与旧实现逐条相同。 + let private appendRumor (nowTick: int64) (rumor: RumorEvent) (world: World) : World = + let cutoffDay = max 0L (rumorDayIndex nowTick - rumorRetentionDays) + let nextCount = world.RumorCount + 1 + let nextOldest = min world.RumorOldestDay rumor.DayIndex + if nextCount > rumorCapacity || nextOldest < cutoffDay then + let trimmed = trimRumors nowTick (rumor :: world.Rumors) + { world with + Rumors = trimmed + RumorCount = trimmed.Length + RumorOldestDay = oldestRumorDay trimmed } + else + { world with + Rumors = rumor :: world.Rumors + RumorCount = nextCount + RumorOldestDay = nextOldest } + + /// `NpcId`/`RumorId` 是 `[]` 单例判别联合:直接用 `=` 比较会走泛型结构相等并 + /// 在每次比较时为两侧装箱(实测 ~24 B/次)。在按 tick 全表扫描的热路径里逐条比较, + /// 这是 Rumor 分配的主要来源。这里改为先解构出底层 int/int64 再比较(语义完全等价, + /// 仅去掉装箱),扫描不再产生逐条分配。 + let inline private npcIdValue (NpcId id) : int = id + let inline private rumorIdValue (RumorId id) : int64 = id let private latestRumorFor (receiver: NpcId) (world: World) : RumorEvent option = // 工作集最新在前且 Tick 非递增:第一条命中的即“Tick 最大、Id 最大”,与原实现 // (filter 后 sortByDescending 取头)等价;窗口外无分配提前返回。 + let receiverId = npcIdValue receiver let rec loop (rest: RumorEvent list) : RumorEvent option = match rest with | [] -> None | rumor :: tail -> if world.Tick - rumor.Tick > rumorFreshnessTicks then None - elif rumor.Receiver = receiver + elif npcIdValue rumor.Receiver = receiverId && rumor.Tick <= world.Tick && rumorStrengthAt world.Tick rumor >= rumorMinimumStrength then Some rumor else loop tail @@ -783,11 +821,13 @@ module Sim = let avatarMemory = recordMemory world.Tick (Dialogue(target, intent, response)) world.Avatar.Mind.Memory let event = { Tick = world.Tick; Kind = DialogueEvent outcome } let updated = - { world with - Avatar = { world.Avatar with Mind = { world.Avatar.Mind with Memory = avatarMemory } } - Npcs = newNpcs - Events = world.Events @ [ event ] - Rumors = consRumor world.Tick rumor world.Rumors } + appendRumor + world.Tick + rumor + { world with + Avatar = { world.Avatar with Mind = { world.Avatar.Mind with Memory = avatarMemory } } + Npcs = newNpcs + Events = world.Events @ [ event ] } let withRumorAnnal = appendAnnal { Tick = world.Tick @@ -826,16 +866,21 @@ module Sim = RecentMemory = List.truncate 8 npc.Mind.Memory }) let private rumorIsDuplicate (request: ChatRequest) (parent: RumorEvent option) (world: World) : bool = - let parentId = parent |> Option.map (fun rumor -> rumor.Id) + let parentId = parent |> Option.map (fun rumor -> rumorIdValue rumor.Id) + let narratorId = npcIdValue request.Narrator + let receiverId = npcIdValue request.Receiver // 窗口外条目强度必 < rumorMinimumStrength,与原全表 exists 等价;命中或越窗即无分配返回。 + // `Parent` 用底层 int64 比较而非 `RumorId option` 结构相等,避免逐条装箱。 let rec loop (rest: RumorEvent list) : bool = match rest with | [] -> false | rumor :: tail -> if world.Tick - rumor.Tick > rumorFreshnessTicks then false - elif rumor.Narrator = request.Narrator - && rumor.Receiver = request.Receiver - && rumor.Parent = parentId + elif npcIdValue rumor.Narrator = narratorId + && npcIdValue rumor.Receiver = receiverId + && (match rumor.Parent with + | Some parentRumor -> parentId = Some(rumorIdValue parentRumor) + | None -> parentId.IsNone) && rumor.Tick <= world.Tick && rumorStrengthAt world.Tick rumor >= rumorMinimumStrength then true else loop tail @@ -936,7 +981,7 @@ module Sim = { npc with Mind = { npc.Mind with Memory = recordMemory world.Tick (Rumor rumor.Id) npc.Mind.Memory } } { chatted with Npcs = newNpcs } - let updated = { withReceiverRumor with Rumors = consRumor world.Tick rumor world.Rumors } + let updated = appendRumor world.Tick rumor withReceiverRumor ChatSucceeded( Some rumor, appendAnnal -- cgit v1.2.3