diff --git a/translator/src/Translator.Cli/Program.cs b/translator/src/Translator.Cli/Program.cs index b710b1e..2e00de5 100644 --- a/translator/src/Translator.Cli/Program.cs +++ b/translator/src/Translator.Cli/Program.cs @@ -139,6 +139,15 @@ TranslationOptions WithProjectFrontEndPolicy( LeafInliningBlockedTargets = leafInliningBlockedTargets.Value }; string? translateModInputCacheDirectory = null; +// translate-mod reads the same (large) base metadata from several places. +var baseMetadataCache = new Dictionary(StringComparer.OrdinalIgnoreCase); +BaseTranslationOutputMetadata ReadBaseMetadataCached(string path) +{ + var fullPath = Path.GetFullPath(path); + if (!baseMetadataCache.TryGetValue(fullPath, out var metadata)) + baseMetadataCache[fullPath] = metadata = BaseTranslationOutputMetadataFile.Read(fullPath); + return metadata; +} string? translateModPublishedOutputDirectory = null; List? _executableRanges = null; // Callback discovery now runs on the discovery worker pool, so the range cache @@ -472,10 +481,6 @@ int RunTranslateRecursive(string[] argsTail) var pruneStale = HasFlag(argsTail, "--prune-stale"); var threadsOption = OptionValue(argsTail, "--threads"); var maxThreads = threadsOption != null ? Math.Max(1, ParseInt(threadsOption)) : Math.Max(1, Environment.ProcessorCount); - // Discovery batches: cheap front-end-only work items, so the batch is sized - // generously to keep queue overhead off the hot path. Deliberately larger - // than the mod emission wave below, which carries whole C++ emissions. - var batchSize = Math.Max(64, maxThreads * 8); Console.WriteLine($"[translator] Using up to {maxThreads} worker thread(s)."); // Log start time @@ -493,7 +498,6 @@ int RunTranslateRecursive(string[] argsTail) // callees are decoded separately and never appear here). Feeds the // fall-through interior-entry detection for the base manifest. var translatedFlowCoverage = new Dictionary(); - var gqrUnknownEntryRoots = new HashSet { startAddr }; var translatedFunctionEnds = new Dictionary(); // Every map entry is a function start, so the whole boundary set is known // before the first decode instead of accumulating as discovery proceeds. @@ -565,13 +569,15 @@ int RunTranslateRecursive(string[] argsTail) void ProcessQueue(bool speculative = false) { + // Each round decodes the whole frontier: the output depends only on the final + // boundary set (boundary repair re-decodes stale owners), never on batching. while (queue.Count > 0) { - var batch = new List<(uint Address, int Depth, string Name)>(batchSize); + var batch = new List<(uint Address, int Depth, string Name)>(queue.Count); var batchAddresses = new HashSet(); var candidates = QueueBatch.Dequeue( queue, - batchSize, + queue.Count, next => ShouldSkip(next) || !batchAddresses.Add(next.addr)); foreach (var next in candidates) { @@ -598,23 +604,19 @@ int RunTranslateRecursive(string[] argsTail) } } - // Batches drain one at a time so a callee of a prior speculative entry arrives with a known - // caller/GQR state instead of as another unknown root. Everything here is speculative, so a - // "call" decoded from an unreached map entry isn't proven code; treat failures as soft, not fatal. + // Everything here is speculative, so a "call" decoded from an unreached map entry + // isn't proven code; treat failures as soft, not fatal. void ProcessSpeculativeSeeds() { while (speculativeSeeds.Count > 0) { - var batch = new List<(uint Address, int Depth, string Name)>(batchSize); + var batch = new List<(uint Address, int Depth, string Name)>(speculativeSeeds.Count); var candidates = QueueBatch.Dequeue( speculativeSeeds, - batchSize, + speculativeSeeds.Count, address => translated.Contains(address) || !visited.Add(address)); foreach (var address in candidates) { - // Nothing calls this from the translated set, so its entry - // GQR state is unconstrained. - gqrUnknownEntryRoots.Add(address); batch.Add((address, 0, ResolveFunctionName(address, preferred: null, $"func_{address:X8}"))); } @@ -652,22 +654,42 @@ int RunTranslateRecursive(string[] argsTail) // decoded out of one - is dropped with a counted warning. void TranslateBatch(List<(uint Address, int Depth, string Name)> batch, bool speculative = false) { - var results = new FunctionDiscoveryResult?[batch.Count]; - IndexedParallel.For(batch.Count, parallelOptions, i => results[i] = Attempt(batch[i], speculative)); + var metrics = new TranslationMetrics?[batch.Count]; + var entryPoints = new uint[batch.Count]; + var discoveryTargets = new uint[batch.Count][]; + var swallowedSiblingEntries = new uint[batch.Count][]; + var summaries = new GqrFunctionSummary[batch.Count]; + var functionEnds = new uint[batch.Count]; + var flowCoverage = new uint[batch.Count][]; + + // Everything kept from a decode is derived in the decode's own pass, so the full + // result is never retained; the few results a newly revealed boundary invalidates + // are simply decoded again. Commits stay serial and in batch order. + void Decode(int i, bool findSwallowedSiblings) + { + var result = Attempt(batch[i], speculative); + metrics[i] = result?.Metrics; + if (result is null) + { + canonicalIrStore.Remove(batch[i].Address); + return; + } + entryPoints[i] = result.EntryPoint; + if (findSwallowedSiblings) + swallowedSiblingEntries[i] = DiscoverSwallowedSiblingEntries(result).ToArray(); + discoveryTargets[i] = DiscoverDiscoveryTranslationTargets(result).ToArray(); + summaries[i] = GqrFunctionSummary.Create(result.Ssa.Function); + canonicalIrStore.Put(batch[i].Address, result.Ssa.Function); + functionEnds[i] = GetFunctionInstructionEnd(result); + flowCoverage[i] = CoalesceInstructionRanges(result); + } + + IndexedParallel.For(batch.Count, parallelOptions, i => Decode(i, findSwallowedSiblings: true)); // Re-run only translations that swallowed a newly revealed sibling entry, or direct - // branches could bypass a mod overlay registered there. Both scans are pure per-result - // so they run on the same worker pool; the dedup filter below keeps first sighting. - var discoveryTargets = new uint[results.Length][]; - var swallowedSiblingEntries = new uint[results.Length][]; - IndexedParallel.For(results.Length, parallelOptions, i => - { - if (results[i] is not { } result) return; - discoveryTargets[i] = DiscoverDiscoveryTranslationTargets(result).ToArray(); - swallowedSiblingEntries[i] = DiscoverSwallowedSiblingEntries(result).ToArray(); - }); - var newlyKnownEntries = Enumerable.Range(0, results.Length) - .Where(i => results[i] is not null) + // branches could bypass a mod overlay registered there. The dedup filter keeps first sighting. + var newlyKnownEntries = Enumerable.Range(0, batch.Count) + .Where(i => metrics[i] is not null) .SelectMany(i => discoveryTargets[i].Concat(swallowedSiblingEntries[i])) .Where(knownBaseFunctionEntryPoints.Add) .ToHashSet(); @@ -675,49 +697,39 @@ int RunTranslateRecursive(string[] argsTail) { // The boundary set is final for this batch once the filter above has run, // so the affected re-translations are independent of one another. - var affected = new List(); - for (var i = 0; i < results.Length; i++) - { - if (results[i] is not { } result) continue; - var entry = result.EntryPoint; - if (result.Instructions.Any(instruction => - instruction.Address != entry && newlyKnownEntries.Contains(instruction.Address))) - { - affected.Add(i); - } - } - - IndexedParallel.For(affected.Count, parallelOptions, k => - { - var i = affected[k]; - results[i] = Attempt(batch[i], speculative); - if (results[i] is { } result) - { - discoveryTargets[i] = DiscoverDiscoveryTranslationTargets(result).ToArray(); - } - }); + var sortedNewEntries = newlyKnownEntries.Order().ToArray(); + var affected = Enumerable.Range(0, batch.Count) + .Where(i => metrics[i] is not null && + CoversInteriorInstruction(flowCoverage[i], entryPoints[i], sortedNewEntries)) + .ToArray(); + IndexedParallel.For(affected.Length, parallelOptions, k => Decode(affected[k], findSwallowedSiblings: false)); } - // The compact GQR projection is a pure walk of the final SSA graph and the - // canonical store serializes its own dictionary, so both move off the serial - // commit. Every address in a batch is distinct and untranslated, so the store - // sees one Put per key regardless of completion order. - var summaries = new GqrFunctionSummary[results.Length]; - IndexedParallel.For(results.Length, parallelOptions, i => - { - if (results[i] is not { } result) return; - summaries[i] = GqrFunctionSummary.Create(result.Ssa.Function); - canonicalIrStore.Put(batch[i].Address, result.Ssa.Function); - }); - - // Update state sequentially (thread-safe) for (var i = 0; i < batch.Count; i++) { - if (results[i] is not { } result) continue; - CommitResult(batch[i], result, discoveryTargets[i], summaries[i]); + if (metrics[i] is not { } functionMetrics) continue; + CommitResult(batch[i], functionMetrics, discoveryTargets[i], summaries[i], functionEnds[i], flowCoverage[i]); } } + // True when an instruction of the coalesced [start, end) coverage, other than the + // entry, starts at one of the sorted addresses. + static bool CoversInteriorInstruction(uint[] coverage, uint entry, uint[] sortedAddresses) + { + for (var r = 0; r < coverage.Length; r += 2) + { + var index = Array.BinarySearch(sortedAddresses, coverage[r]); + for (index = index >= 0 ? index : ~index; + index < sortedAddresses.Length && sortedAddresses[index] < coverage[r + 1]; + index++) + { + var address = sortedAddresses[index]; + if (address != entry && (address & 3u) == 0) return true; + } + } + return false; + } + FunctionDiscoveryResult? Attempt((uint Address, int Depth, string Name) work, bool speculative) { if (!speculative) @@ -773,16 +785,18 @@ int RunTranslateRecursive(string[] argsTail) void CommitResult( (uint Address, int Depth, string Name) work, - FunctionDiscoveryResult result, + TranslationMetrics functionMetrics, IReadOnlyList discoveryTargets, - GqrFunctionSummary summary) + GqrFunctionSummary summary, + uint functionEnd, + uint[] flowCoverage) { // File writing moved to parallel section above - totals.Add(result.Metrics); + totals.Add(functionMetrics); count++; translated.Add(work.Address); - translatedFunctionEnds[work.Address] = GetFunctionInstructionEnd(result); - translatedFlowCoverage[work.Address] = CoalesceInstructionRanges(result); + translatedFunctionEnds[work.Address] = functionEnd; + translatedFlowCoverage[work.Address] = flowCoverage; foreach (var target in discoveryTargets) { @@ -821,6 +835,7 @@ int RunTranslateRecursive(string[] argsTail) // list that existed to paper over the addresses they still missed. ProcessQueue(); var walkReachable = count; + var walkTranslated = translated.ToHashSet(); ProcessSpeculativeSeeds(); if (functionMap is not null) { @@ -864,26 +879,31 @@ int RunTranslateRecursive(string[] argsTail) if (boundaryRepairAddresses.Length != 0) { Console.WriteLine($"[translator] Reanalyzing {boundaryRepairAddresses.Length:N0} function(s) affected by later-discovered boundaries..."); - var repaired = new FunctionDiscoveryResult[boundaryRepairAddresses.Length]; + var repairedSummaries = new GqrFunctionSummary[boundaryRepairAddresses.Length]; + var repairedEnds = new uint[boundaryRepairAddresses.Length]; + var repairedCoverage = new uint[boundaryRepairAddresses.Length][]; IndexedParallel.For(boundaryRepairAddresses.Length, parallelOptions, i => { var address = boundaryRepairAddresses[i]; var work = baseWork[address]; - repaired[i] = translator.Value.Discover(address, WithProjectFrontEndPolicy(new TranslationOptions( + var repaired = translator.Value.Discover(address, WithProjectFrontEndPolicy(new TranslationOptions( work.Name, MaxInstructions: work.MaxInstructions, MaxBytes: work.MaxBytes, AllowUnsupportedInstructions: work.AllowUnsupported, KnownFunctionEntryPoints: knownBaseFunctionEntryPoints), RequireProject().Translation)); + repairedSummaries[i] = GqrFunctionSummary.Create(repaired.Ssa.Function); + canonicalIrStore.Put(address, repaired.Ssa.Function); + repairedEnds[i] = GetFunctionInstructionEnd(repaired); + repairedCoverage[i] = CoalesceInstructionRanges(repaired); }); for (var i = 0; i < boundaryRepairAddresses.Length; i++) { var address = boundaryRepairAddresses[i]; - baseSummaries[address] = GqrFunctionSummary.Create(repaired[i].Ssa.Function); - canonicalIrStore.Put(address, repaired[i].Ssa.Function); - translatedFunctionEnds[address] = GetFunctionInstructionEnd(repaired[i]); - translatedFlowCoverage[address] = CoalesceInstructionRanges(repaired[i]); + baseSummaries[address] = repairedSummaries[i]; + translatedFunctionEnds[address] = repairedEnds[i]; + translatedFlowCoverage[address] = repairedCoverage[i]; } } @@ -906,6 +926,12 @@ int RunTranslateRecursive(string[] argsTail) } Console.WriteLine("[translator] Computing interprocedural GQR constants and callee write summaries..."); + // Map entries the walk never reached have an unconstrained entry GQR state unless some + // translated function calls them directly. + var gqrUnknownEntryRoots = GqrInterproceduralAnalysis.SelectUnknownEntryRoots( + baseSummaries, + [startAddr], + (functionMap?.Addresses ?? Array.Empty()).Where(address => !walkTranslated.Contains(address))); var gqrAnalysis = GqrInterproceduralAnalysis.Analyze( baseSummaries, gqrUnknownEntryRoots); @@ -1183,7 +1209,7 @@ int RunTranslateRecursive(string[] argsTail) // Populate the recursive ABI inference cache once, in stable global order, before // refinement and parallel emission, so cyclic call graphs don't depend on thread count. LogPhase("state-free selection + leaf residency"); - ReportAbiPrewarm(inferredGuestAbi.Value.Prewarm(emissionAddresses)); + ReportAbiPrewarm(inferredGuestAbi.Value.Prewarm(emissionAddresses, maxThreads)); LogPhase("ABI prewarm"); // State-free entries use the transformed leaf's exact live-at-entry @@ -1404,6 +1430,7 @@ int RunTranslateRecursive(string[] argsTail) // source visible. `completionPulse` only wakes the writer; it carries // no state of its own. var emissionDone = new bool[pendingSources.Length]; + var unsupportedByIndex = new int[emissionAddresses.Length]; using var completionPulse = new ManualResetEventSlim(false); var emissionAborted = false; Task? pendingBundleWrite = null; @@ -1459,6 +1486,8 @@ int RunTranslateRecursive(string[] argsTail) var options = BuildEmissionOptions(address, stateFreeAbiContracts); if (!canonicalIrStore.TryGet(address, out var canonicalSsa)) throw new InvalidOperationException($"Missing canonical SSA for final lowering at 0x{address:X8}."); + unsupportedByIndex[i] = canonicalSsa.Blocks.Sum(block => + block.Instructions.Count(static instruction => instruction is IrUndefined)); var emitted = translator.Value.LowerCanonical(address, canonicalSsa, options); var path = work.Path; if (outputMetadata is not null) @@ -1517,16 +1546,7 @@ int RunTranslateRecursive(string[] argsTail) : Path.GetRelativePath( Path.GetDirectoryName(Path.GetFullPath(outputMetadataPath!))!, Path.GetFullPath(productionSourceBundlePath)).Replace('\\', '/'); - var metadataUnsupportedInstructionCount = emissionAddresses.Sum(address => - { - if (!canonicalIrStore.TryGet(address, out var canonicalSsa)) - { - throw new InvalidOperationException( - $"Missing canonical SSA while computing translation quality for 0x{address:X8}."); - } - return canonicalSsa.Blocks.Sum(block => - block.Instructions.Count(static instruction => instruction is IrUndefined)); - }); + var metadataUnsupportedInstructionCount = unsupportedByIndex.Sum(); completedOutputMetadata = BaseTranslationOutputMetadata.Create( outputMetadata, new TranslationQualityMetadata( @@ -1701,7 +1721,7 @@ int RunTranslateModCore(string[] argsTail, string? outputDirectoryOverride) // win (this shipped once as a character-select LinkList.h:573 panic). if (File.Exists(baseTranslationOutputMetadataPath) && File.Exists(codePul)) { - var guardedBaseMetadata = BaseTranslationOutputMetadataFile.Read(baseTranslationOutputMetadataPath); + var guardedBaseMetadata = ReadBaseMetadataCached(baseTranslationOutputMetadataPath); var codePulSha256 = ChecksumUtilities.Sha256HexOfFile(codePul); var baseKnowsThisPul = guardedBaseMetadata.ModPatchAwareness is { } awareness && awareness.Any(entry => string.Equals(entry.CodePulSha256, codePulSha256, StringComparison.OrdinalIgnoreCase)); @@ -2079,7 +2099,7 @@ void WriteResolvedDispatchProfile( var metadataPath = baseTranslationOutputMetadataPath; if (File.Exists(metadataPath)) { - var metadata = BaseTranslationOutputMetadataFile.Read(metadataPath); + var metadata = ReadBaseMetadataCached(metadataPath); if (!string.IsNullOrWhiteSpace(metadata.SourceBundlePath)) { var bundlePath = Path.IsPathRooted(metadata.SourceBundlePath) @@ -2154,7 +2174,7 @@ void WriteResolvedDispatchProfile( BaseTranslationOutputMetadata? metadata = null; if (File.Exists(baseTranslationOutputMetadataPath)) { - metadata = BaseTranslationOutputMetadataFile.Read(baseTranslationOutputMetadataPath); + metadata = ReadBaseMetadataCached(baseTranslationOutputMetadataPath); } if (metadata is null || metadata.Functions.Any(static function => function.Build is null)) { @@ -2326,12 +2346,9 @@ int EmitModCpp( : new CompositeGuestFunctionAbiProvider(inferredAbi); var modTranslator = new FunctionTranslator(modImage, abiProvider); var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = maxThreads }; - // Mod emission waves: each item is a full translate+emit, and each wave is a - // barrier that also drains newly discovered continuations, so it is - // deliberately smaller than the discovery batch above - a long wave delays - // the discoveries the next wave has to translate. - var waveSize = Math.Max(16, maxThreads * 4); - Console.WriteLine($" translating mod C++ with up to {maxThreads} worker thread(s) in waves of {waveSize}"); + // Each wave drains its whole queue: a wave is a barrier that publishes newly discovered + // entries, and draining everything makes the output independent of any wave size. + Console.WriteLine($" translating mod C++ with up to {maxThreads} worker thread(s)"); var cppRoot = Path.Combine(outDir, "cpp"); if (Directory.Exists(cppRoot)) @@ -2410,7 +2427,7 @@ int EmitModCpp( // Some dynamically discovered module/continuation waves bypass // ProcessBoundedWaves. Prewarm every parallel emission boundary as a // final deterministic guard; phase-prewarmed entries are cheap hits. - ReportAbiPrewarm(inferredAbi.Prewarm(wave.Select(static work => work.Address))); + ReportAbiPrewarm(inferredAbi.Prewarm(wave.Select(static work => work.Address), maxThreads)); return ModTranslationWaveRunner.Translate(() => modTranslator, wave, parallelOptions); } @@ -2447,7 +2464,7 @@ int EmitModCpp( // Populate the recursive ABI cache in a stable global order before any worker emits C++, // so shared closure reuse and cyclic call graphs never depend on thread count, wave size, // or worker order. - ReportAbiPrewarm(inferredAbi.Prewarm(ordered.Select(static work => work.Address))); + ReportAbiPrewarm(inferredAbi.Prewarm(ordered.Select(static work => work.Address), maxThreads)); // Commit callbacks here only append to the discovered-continuation queue that a later // phase drains, so nothing in this phase reads back its own commits; wave barriers would // only cost idle workers, so it runs as one parallel batch and the runner re-sorts by the @@ -2592,7 +2609,7 @@ int EmitModCpp( { while (discoveredContinuationQueue.Count > 0) { - var wave = QueueBatch.Dequeue(discoveredContinuationQueue, waveSize) + var wave = QueueBatch.Dequeue(discoveredContinuationQueue, discoveredContinuationQueue.Count) .Select(CreateContinuationWork) .ToArray(); CommitWave(TranslateWave(wave), (result, work) => @@ -2611,10 +2628,10 @@ int EmitModCpp( var emittedModuleStarts = new HashSet(); while (moduleQueue.Count > 0) { - var wave = new List(waveSize); + var wave = new List(moduleQueue.Count); foreach (var start in QueueBatch.Dequeue( moduleQueue, - waveSize, + moduleQueue.Count, start => !emittedModuleStarts.Add(start.Address))) { var name = ModuleFunctionName(start.Address); diff --git a/translator/src/Translator.Cli/Translator.Cli.csproj b/translator/src/Translator.Cli/Translator.Cli.csproj index 6543c9f..2238d6e 100644 --- a/translator/src/Translator.Cli/Translator.Cli.csproj +++ b/translator/src/Translator.Cli/Translator.Cli.csproj @@ -8,6 +8,7 @@ true true true + false 0.5.0 WiiCompiled Mario Kart Wii Static Translator diff --git a/translator/src/Translator.Core/CodeGen/GuestMemoryRangeLowering.cs b/translator/src/Translator.Core/CodeGen/GuestMemoryRangeLowering.cs index 010a821..9639598 100644 --- a/translator/src/Translator.Core/CodeGen/GuestMemoryRangeLowering.cs +++ b/translator/src/Translator.Core/CodeGen/GuestMemoryRangeLowering.cs @@ -102,12 +102,7 @@ public static class GuestMemoryRangeLowering .Any(group => group.Count() >= minimumAccesses)) return function; - var cfg = IrCfg.Build(function); - var dominators = ComputeDominators(cfg); - var reachability = ComputeReachability(cfg); - var cyclicBlocks = cfg.Blocks.Keys.Where(label => - cfg.Successors(label).Any(successor => reachability[successor].Contains(label))) - .ToHashSet(StringComparer.OrdinalIgnoreCase); + var graph = BlockGraph.Build(function); var plans = new Dictionary<(int Block, int Instruction), Plan>(); var resolves = new Dictionary>(); @@ -129,24 +124,35 @@ public static class GuestMemoryRangeLowering if (definitionSites.Count != 1) continue; definition = definitionSites[0]; var defined = definitionSites[0]; - var definitionBlock = function.Blocks[defined.BlockIndex].Label; if (remaining.Any(candidate => - !dominators[candidate.AccessBlockLabel(function)].Contains(definitionBlock) || + !graph.Dominates(defined.BlockIndex, candidate.BlockIndex) || candidate.BlockIndex == defined.BlockIndex && candidate.Access.Index <= defined.InstructionIndex)) continue; } + // The pair predicate is invariant across rounds, so memoize it. + var groupIndex = new Dictionary(ReferenceEqualityComparer.Instance); + foreach (var access in remaining) groupIndex.Add(access, groupIndex.Count); + var compatible = remaining.Count <= 4096 ? new sbyte[remaining.Count * remaining.Count] : null; + bool Compatible(FunctionAccess anchor, FunctionAccess candidate) + { + if (compatible is null) + return DominatesAccess(graph, anchor, candidate) && + !HasInvalidatingBoundaryOnPath(graph, invalidators, anchor, candidate); + var slot = groupIndex[anchor] * groupIndex.Count + groupIndex[candidate]; + if (compatible[slot] != 0) return compatible[slot] > 0; + var value = DominatesAccess(graph, anchor, candidate) && + !HasInvalidatingBoundaryOnPath(graph, invalidators, anchor, candidate); + compatible[slot] = value ? (sbyte)1 : (sbyte)-1; + return value; + } while (remaining.Count >= minimumAccesses) { FunctionAccess? bestAnchor = null; FunctionAccess[] bestMembers = []; foreach (var anchor in remaining) { - var members = remaining.Where(candidate => - DominatesAccess(function, dominators, anchor, candidate) && - !HasInvalidatingBoundaryOnPath( - function, reachability, cyclicBlocks, - invalidators, anchor, candidate)) + var members = remaining.Where(candidate => Compatible(anchor, candidate)) .OrderBy(candidate => candidate.Access.Index) .ToList(); // Keep a bounded affine window around the real anchor. A @@ -248,15 +254,13 @@ public static class GuestMemoryRangeLowering } private static bool DominatesAccess( - IrFunction function, - IReadOnlyDictionary> dominators, + BlockGraph graph, FunctionAccess anchor, FunctionAccess consumer) { if (anchor.BlockIndex == consumer.BlockIndex) return anchor.Access.Index <= consumer.Access.Index; - return dominators[consumer.AccessBlockLabel(function)].Contains( - anchor.AccessBlockLabel(function)); + return graph.Dominates(anchor.BlockIndex, consumer.BlockIndex); } private static bool HasOverlappingOppositeAccess( @@ -273,27 +277,25 @@ public static class GuestMemoryRangeLowering } private static bool HasInvalidatingBoundaryOnPath( - IrFunction function, - IReadOnlyDictionary> reachability, - IReadOnlySet cyclicBlocks, + BlockGraph graph, IReadOnlyList invalidators, FunctionAccess anchor, FunctionAccess consumer) { - var anchorLabel = anchor.AccessBlockLabel(function); - var consumerLabel = consumer.AccessBlockLabel(function); + var anchorBlock = anchor.BlockIndex; + var consumerBlock = consumer.BlockIndex; + var consumerReentered = anchorBlock != consumerBlock && graph.Cyclic[consumerBlock]; foreach (var invalidator in invalidators) { - var boundaryLabel = function.Blocks[invalidator.BlockIndex].Label; - var afterAnchor = invalidator.BlockIndex == anchor.BlockIndex + var boundaryBlock = invalidator.BlockIndex; + var afterAnchor = boundaryBlock == anchorBlock ? invalidator.InstructionIndex > anchor.Access.Index - : reachability[anchorLabel].Contains(boundaryLabel); - var beforeConsumer = invalidator.BlockIndex == consumer.BlockIndex - ? invalidator.InstructionIndex < consumer.Access.Index || - anchor.BlockIndex != consumer.BlockIndex && - cyclicBlocks.Contains(consumerLabel) - : reachability[boundaryLabel].Contains(consumerLabel); - if (afterAnchor && beforeConsumer) return true; + : graph.Reaches(anchorBlock, boundaryBlock); + if (!afterAnchor) continue; + var beforeConsumer = boundaryBlock == consumerBlock + ? invalidator.InstructionIndex < consumer.Access.Index || consumerReentered + : graph.Reaches(boundaryBlock, consumerBlock); + if (beforeConsumer) return true; } return false; } @@ -315,54 +317,117 @@ public static class GuestMemoryRangeLowering return result; } - private static Dictionary> ComputeDominators(IrCfg cfg) + /// Dominator and reachability sets as bitsets over block ordinals. + private sealed class BlockGraph { - var comparer = StringComparer.OrdinalIgnoreCase; - var labels = cfg.Blocks.Keys.ToArray(); - var all = labels.ToHashSet(comparer); - var result = labels.ToDictionary(label => label, - label => label.Equals(cfg.Entry, StringComparison.OrdinalIgnoreCase) - ? new HashSet([label], comparer) - : new HashSet(all, comparer), comparer); - bool changed; - do - { - changed = false; - foreach (var label in labels) - { - if (label.Equals(cfg.Entry, StringComparison.OrdinalIgnoreCase)) continue; - var predecessors = cfg.Predecessors(label); - var next = predecessors.Count == 0 - ? new HashSet(comparer) - : new HashSet(result[predecessors[0]], comparer); - foreach (var predecessor in predecessors.Skip(1)) next.IntersectWith(result[predecessor]); - next.Add(label); - if (result[label].SetEquals(next)) continue; - result[label] = next; - changed = true; - } - } while (changed); - return result; - } + private readonly ulong[][] _dominators; + private readonly ulong[][] _reach; - private static Dictionary> ComputeReachability(IrCfg cfg) - { - var comparer = StringComparer.OrdinalIgnoreCase; - var result = new Dictionary>(comparer); - foreach (var start in cfg.Blocks.Keys) + private BlockGraph(ulong[][] dominators, ulong[][] reach, bool[] cyclic) { - var reachable = new HashSet(comparer); - var pending = new Stack(); - pending.Push(start); - while (pending.Count != 0) - { - var current = pending.Pop(); - if (!reachable.Add(current)) continue; - foreach (var successor in cfg.Successors(current)) pending.Push(successor); - } - result[start] = reachable; + _dominators = dominators; + _reach = reach; + Cyclic = cyclic; + } + + /// Blocks that can reach themselves again through a successor. + public bool[] Cyclic { get; } + + public bool Dominates(int dominator, int block) => + (_dominators[block][dominator >> 6] & (1UL << (dominator & 63))) != 0; + + /// Reflexive: every block reaches itself. + public bool Reaches(int from, int to) => + (_reach[from][to >> 6] & (1UL << (to & 63))) != 0; + + public static BlockGraph Build(IrFunction function) + { + var cfg = IrCfg.Build(function); + var count = function.Blocks.Count; + var words = (count + 63) >> 6; + var index = new Dictionary(count, StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < count; i++) index.Add(function.Blocks[i].Label, i); + var successors = new int[count][]; + var predecessors = new int[count][]; + for (var i = 0; i < count; i++) + { + var label = function.Blocks[i].Label; + successors[i] = cfg.Successors(label).Select(successor => index[successor]).ToArray(); + predecessors[i] = cfg.Predecessors(label).Select(predecessor => index[predecessor]).ToArray(); + } + + var reach = new ulong[count][]; + var pending = new Stack(); + for (var start = 0; start < count; start++) + { + var row = new ulong[words]; + pending.Push(start); + while (pending.Count != 0) + { + var current = pending.Pop(); + ref var word = ref row[current >> 6]; + var bit = 1UL << (current & 63); + if ((word & bit) != 0) continue; + word |= bit; + foreach (var successor in successors[current]) pending.Push(successor); + } + reach[start] = row; + } + + var cyclic = new bool[count]; + for (var i = 0; i < count; i++) + { + foreach (var successor in successors[i]) + { + if ((reach[successor][i >> 6] & (1UL << (i & 63))) == 0) continue; + cyclic[i] = true; + break; + } + } + + var entry = index[cfg.Entry]; + var dominators = new ulong[count][]; + for (var i = 0; i < count; i++) + { + dominators[i] = new ulong[words]; + if (i == entry) + { + dominators[i][i >> 6] = 1UL << (i & 63); + continue; + } + Array.Fill(dominators[i], ulong.MaxValue); + if ((count & 63) != 0) dominators[i][words - 1] = (1UL << (count & 63)) - 1; + } + var next = new ulong[words]; + bool changed; + do + { + changed = false; + for (var i = 0; i < count; i++) + { + if (i == entry) continue; + var preds = predecessors[i]; + if (preds.Length == 0) + { + Array.Clear(next); + } + else + { + Array.Copy(dominators[preds[0]], next, words); + for (var p = 1; p < preds.Length; p++) + { + var other = dominators[preds[p]]; + for (var w = 0; w < words; w++) next[w] &= other[w]; + } + } + next[i >> 6] |= 1UL << (i & 63); + if (next.AsSpan().SequenceEqual(dominators[i])) continue; + next.AsSpan().CopyTo(dominators[i]); + changed = true; + } + } while (changed); + return new BlockGraph(dominators, reach, cyclic); } - return result; } private static IrInstruction ToResolved(IrInstruction instruction, Access access, Plan plan) diff --git a/translator/src/Translator.Core/CodeGen/InferredGuestFunctionAbiProvider.cs b/translator/src/Translator.Core/CodeGen/InferredGuestFunctionAbiProvider.cs index f4d9fb0..b8595a4 100644 --- a/translator/src/Translator.Core/CodeGen/InferredGuestFunctionAbiProvider.cs +++ b/translator/src/Translator.Core/CodeGen/InferredGuestFunctionAbiProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Numerics; using System.Threading; using System.Threading.Tasks; using Translator.Core.Analysis; @@ -72,6 +73,9 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider /// Reusable backing list for ; phase A is never reentrant. public readonly List FrameTaintBuffer = new(); + /// Address per entry, innermost last. + public readonly List FrameAddresses = new(); + /// Addresses already known tainted, shared by every phase-A worker of the current Prewarm /// call and reset each invocation. Without this memoization an unpublishable address gets /// re-analyzed, subtree and all, by every caller that reaches it, which compounds combinatorially @@ -95,14 +99,21 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider return false; } + var state = _threadState.Value!; + var frameTaint = state.FrameTaint; + if (frameTaint is not null && state.FrameAddresses.Count > 0 && state.FrameAddresses[^1] == address) + { + // Direct self-recursion sees what a sequential run would, even once another thread has published it. + abi = GuestFunctionAbi.Empty; + return false; + } + if (_cache.TryGetValue(address, out abi!)) { return true; } - var state = _threadState.Value!; var inProgress = state.InProgress; - var frameTaint = state.FrameTaint; if (frameTaint is not null && state.TaintedAddresses!.ContainsKey(address)) { @@ -151,6 +162,7 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider // Prewarm phase A: track taint for this frame and publish only if it stayed clean. frameTaint.Add(false); + state.FrameAddresses.Add(address); var frameIndex = frameTaint.Count - 1; bool succeeded; bool tainted; @@ -172,6 +184,7 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider finally { inProgress.Remove(address); + state.FrameAddresses.RemoveAt(frameIndex); tainted = frameTaint[frameIndex]; frameTaint.RemoveAt(frameIndex); if (tainted && frameIndex > 0) @@ -274,6 +287,7 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider var state = _threadState.Value!; var frames = state.FrameTaintBuffer; frames.Clear(); + state.FrameAddresses.Clear(); state.TaintedAddresses = taintedAddresses; state.FrameTaint = frames; try @@ -418,82 +432,102 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider private HashSet InferArgumentRegisters(IrFunction function, IrCfg cfg) { var comparer = StringComparer.OrdinalIgnoreCase; - var blockUses = new Dictionary>(comparer); - var blockDefs = new Dictionary>(comparer); - - foreach (var block in function.Blocks) + // Interned register ids so the liveness fixpoint runs over bitsets. + var ids = new Dictionary(comparer); + var names = new List(); + int Id(string name) { - var uses = new HashSet(comparer); - var defs = new HashSet(comparer); + if (!ids.TryGetValue(name, out var id)) + { + id = names.Count; + ids.Add(name, id); + names.Add(name); + } + return id; + } + + var blockCount = function.Blocks.Count; + var useLists = new List[blockCount]; + var defLists = new List[blockCount]; + var labelIndex = new Dictionary(blockCount, comparer); + for (var b = 0; b < blockCount; b++) + { + var block = function.Blocks[b]; + var uses = new List(); + var defs = new List(); + var defined = new HashSet(); foreach (var instruction in block.Instructions) { foreach (var use in InstructionRegisterUsesForArgumentInference(instruction)) { - if (!defs.Contains(use)) - { - uses.Add(use); - } + var id = Id(use); + if (!defined.Contains(id)) + uses.Add(id); } foreach (var def in InstructionRegisterDefsForArgumentInference(instruction)) { - defs.Add(def); + var id = Id(def); + if (defined.Add(id)) + defs.Add(id); } } - - blockUses[block.Label] = uses; - blockDefs[block.Label] = defs; + useLists[b] = uses; + defLists[b] = defs; } - var liveIn = function.Blocks.ToDictionary( - static block => block.Label, - _ => new HashSet(comparer), - comparer); - var liveOut = function.Blocks.ToDictionary( - static block => block.Label, - _ => new HashSet(comparer), - comparer); + for (var b = 0; b < blockCount; b++) + labelIndex.Add(function.Blocks[b].Label, b); + var words = Math.Max(1, (names.Count + 63) >> 6); + var useBits = new ulong[blockCount * words]; + var keepBits = new ulong[blockCount * words]; + Array.Fill(keepBits, ulong.MaxValue); + var successors = new int[blockCount][]; + for (var b = 0; b < blockCount; b++) + { + foreach (var id in useLists[b]) useBits[b * words + (id >> 6)] |= 1UL << (id & 63); + foreach (var id in defLists[b]) keepBits[b * words + (id >> 6)] &= ~(1UL << (id & 63)); + var targets = new List(); + foreach (var successor in cfg.Successors(function.Blocks[b].Label)) + if (labelIndex.TryGetValue(successor, out var index)) + targets.Add(index); + successors[b] = targets.ToArray(); + } + + var liveIn = new ulong[blockCount * words]; var changed = true; while (changed) { changed = false; - for (var blockIndex = function.Blocks.Count - 1; blockIndex >= 0; blockIndex--) + for (var b = blockCount - 1; b >= 0; b--) { - var block = function.Blocks[blockIndex]; - var newOut = new HashSet(comparer); - foreach (var successor in cfg.Successors(block.Label)) + var row = b * words; + for (var w = 0; w < words; w++) { - if (liveIn.TryGetValue(successor, out var successorLiveIn)) + ulong liveOut = 0; + foreach (var successor in successors[b]) + liveOut |= liveIn[successor * words + w]; + var value = useBits[row + w] | (liveOut & keepBits[row + w]); + if (value != liveIn[row + w]) { - newOut.UnionWith(successorLiveIn); + liveIn[row + w] = value; + changed = true; } } - - var newIn = new HashSet(blockUses[block.Label], comparer); - var outMinusDefs = new HashSet(newOut, comparer); - outMinusDefs.ExceptWith(blockDefs[block.Label]); - newIn.UnionWith(outMinusDefs); - - if (!newOut.SetEquals(liveOut[block.Label])) - { - liveOut[block.Label] = newOut; - changed = true; - } - - if (!newIn.SetEquals(liveIn[block.Label])) - { - liveIn[block.Label] = newIn; - changed = true; - } } } - return liveIn.TryGetValue(function.EntryLabel, out var entryLive) - ? entryLive - .Where(IsGprOrFprRegister) - .ToHashSet(comparer) - : new HashSet(comparer); + var result = new HashSet(comparer); + if (labelIndex.TryGetValue(function.EntryLabel, out var entry)) + { + for (var id = 0; id < names.Count; id++) + { + if ((liveIn[entry * words + (id >> 6)] & (1UL << (id & 63))) != 0 && IsGprOrFprRegister(names[id])) + result.Add(names[id]); + } + } + return result; } private IEnumerable InstructionRegisterUsesForArgumentInference(IrInstruction instruction) @@ -755,45 +789,74 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider private HashSet InferScalarFloatArguments(IrFunction function, IrCfg cfg) { var scalarArgs = new HashSet(StringComparer.OrdinalIgnoreCase); - var comparer = StringComparer.OrdinalIgnoreCase; - var inMap = function.Blocks.ToDictionary( - static b => b.Label, - b => b.Label.Equals(function.EntryLabel, StringComparison.OrdinalIgnoreCase) - ? CreateInitialArgumentAliasState() - : new Dictionary>(comparer), - comparer); - var outMap = function.Blocks.ToDictionary( - static b => b.Label, - b => b.Label.Equals(function.EntryLabel, StringComparison.OrdinalIgnoreCase) - ? CreateInitialArgumentAliasState() - : new Dictionary>(comparer), - comparer); + var keys = new AliasKeys(function); + var blockCount = function.Blocks.Count; + var blockIndex = new Dictionary(blockCount, StringComparer.OrdinalIgnoreCase); + for (var b = 0; b < blockCount; b++) + blockIndex[function.Blocks[b].Label] = b; + var predecessors = new int[blockCount][]; + var isEntry = new bool[blockCount]; + for (var b = 0; b < blockCount; b++) + { + var block = function.Blocks[b]; + isEntry[b] = block.Label.Equals(function.EntryLabel, StringComparison.OrdinalIgnoreCase); + var list = new List(); + foreach (var predecessor in cfg.Predecessors(block.Label)) + if (blockIndex.TryGetValue(predecessor, out var index)) + list.Add(index); + predecessors[b] = list.ToArray(); + } + + // Per alias key: -1 when absent, otherwise the mask of ABI float argument sources. + var initial = keys.InitialState(); + var inMap = new int[blockCount][]; + var outMap = new int[blockCount][]; + for (var b = 0; b < blockCount; b++) + { + inMap[b] = isEntry[b] ? (int[])initial.Clone() : keys.EmptyState(); + outMap[b] = isEntry[b] ? (int[])initial.Clone() : keys.EmptyState(); + } var changed = true; while (changed) { changed = false; - foreach (var block in function.Blocks) + for (var b = 0; b < blockCount; b++) { - var newIn = block.Label.Equals(function.EntryLabel, StringComparison.OrdinalIgnoreCase) - ? CreateInitialArgumentAliasState() - : UnionPredecessorStates(cfg.Predecessors(block.Label), outMap); - - var newOut = CopyAliasState(newIn); - foreach (var ins in block.Instructions) + int[] newIn; + if (isEntry[b]) { - ProcessScalarArgumentInstruction(ins, newOut, scalarArgs); + newIn = (int[])initial.Clone(); + } + else + { + newIn = keys.EmptyState(); + foreach (var predecessor in predecessors[b]) + { + var predState = outMap[predecessor]; + for (var key = 0; key < newIn.Length; key++) + { + if (predState[key] < 0) continue; + newIn[key] = newIn[key] < 0 ? predState[key] : newIn[key] | predState[key]; + } + } } - if (!AliasStatesEqual(newIn, inMap[block.Label])) + var newOut = (int[])newIn.Clone(); + foreach (var ins in function.Blocks[b].Instructions) { - inMap[block.Label] = newIn; + ProcessScalarArgumentInstruction(ins, keys, newOut, scalarArgs); + } + + if (!newIn.AsSpan().SequenceEqual(inMap[b])) + { + inMap[b] = newIn; changed = true; } - if (!AliasStatesEqual(newOut, outMap[block.Label])) + if (!newOut.AsSpan().SequenceEqual(outMap[b])) { - outMap[block.Label] = newOut; + outMap[b] = newOut; changed = true; } } @@ -802,63 +865,153 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider return scalarArgs; } + /// Interned float register names the scalar-argument alias analysis can track. + /// Ids 0..12 are the ABI float argument registers, so a source set is a 13-bit mask. + private sealed class AliasKeys + { + private readonly Dictionary _ids = new(StringComparer.OrdinalIgnoreCase); + + public AliasKeys(IrFunction function) + { + foreach (var register in AbiFloatArgumentRegisters) + _ids.Add(register, _ids.Count); + foreach (var block in function.Blocks) + { + foreach (var instruction in block.Instructions) + { + switch (instruction) + { + case IrAssign assign: + AddName(assign.Destination); + AddValue(assign.Value); + break; + case IrBinary binary: + AddValue(binary.Left); + AddValue(binary.Right); + AddName(binary.Destination); + break; + case IrLoad load: + AddName(load.Destination); + break; + case IrStore store: + AddValue(store.Source); + break; + case IrCall call: + foreach (var argument in call.Arguments) AddValue(argument); + AddName(call.Destination); + break; + case IrIndirectCall indirectCall: + foreach (var argument in indirectCall.Arguments) AddValue(argument); + AddName(indirectCall.Destination); + break; + case IrSetCrField setCr: + AddValue(setCr.Left); + AddValue(setCr.Right); + break; + case IrPhi phi: + AddName(phi.Destination); + break; + } + } + } + } + + public int Count => _ids.Count; + + public int[] EmptyState() + { + var state = new int[_ids.Count]; + Array.Fill(state, -1); + return state; + } + + public int[] InitialState() + { + var state = EmptyState(); + for (var i = 0; i < AbiFloatArgumentRegisters.Length; i++) + state[i] = 1 << i; + return state; + } + + public bool TryGet(string baseName, out int id) => _ids.TryGetValue(baseName, out id); + + private void AddValue(IrValue? value) + { + if (value is { Kind: "register", RegisterName: { } name }) + AddName(name); + } + + private void AddName(string? name) + { + if (string.IsNullOrEmpty(name)) return; + var baseName = Base(name); + if (IsFloatRegister(baseName)) + _ids.TryAdd(baseName, _ids.Count); + } + } + private void ProcessScalarArgumentInstruction( IrInstruction ins, - Dictionary> aliases, + AliasKeys keys, + int[] aliases, HashSet scalarArgs) { switch (ins) { case IrAssign assign: - PropagateFloatAlias(assign.Destination, assign.Value, aliases); + PropagateFloatAlias(assign.Destination, assign.Value, keys, aliases); break; case IrBinary bin: - MarkScalarUse(bin.Left, aliases, scalarArgs); - MarkScalarUse(bin.Right, aliases, scalarArgs); - KillFloatAlias(bin.Destination, aliases); + MarkScalarUse(bin.Left, keys, aliases, scalarArgs); + MarkScalarUse(bin.Right, keys, aliases, scalarArgs); + KillFloatAlias(bin.Destination, keys, aliases); break; case IrLoad load: - KillFloatAlias(load.Destination, aliases); + KillFloatAlias(load.Destination, keys, aliases); break; case IrStore store: if (store.SizeBytes is 4 or 8) { - MarkScalarUse(store.Source, aliases, scalarArgs); + MarkScalarUse(store.Source, keys, aliases, scalarArgs); } break; case IrCall call: - ProcessScalarArgumentCall(call.Target, call.Arguments, aliases, scalarArgs); - KillFloatAlias(call.Destination, aliases); + ProcessScalarArgumentCall(call.Target, call.Arguments, keys, aliases, scalarArgs); + KillFloatAlias(call.Destination, keys, aliases); break; case IrIndirectCall icall: foreach (var arg in icall.Arguments) { - MarkScalarUse(arg, aliases, scalarArgs); + MarkScalarUse(arg, keys, aliases, scalarArgs); } - aliases.Remove("f1"); - KillFloatAlias(icall.Destination, aliases); + aliases[F1AliasKey] = -1; + KillFloatAlias(icall.Destination, keys, aliases); break; case IrSetCrField setCr: - MarkScalarUse(setCr.Left, aliases, scalarArgs); - MarkScalarUse(setCr.Right, aliases, scalarArgs); + MarkScalarUse(setCr.Left, keys, aliases, scalarArgs); + MarkScalarUse(setCr.Right, keys, aliases, scalarArgs); break; case IrPhi phi: - KillFloatAlias(phi.Destination, aliases); + KillFloatAlias(phi.Destination, keys, aliases); break; } } + /// Alias key of f1, the first ABI float argument register. + private const int F1AliasKey = 0; + private void ProcessScalarArgumentCall( string target, IReadOnlyList arguments, - Dictionary> aliases, + AliasKeys keys, + int[] aliases, HashSet scalarArgs) { if (IsGuestCallTarget(target)) @@ -873,14 +1026,14 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider arg.RegisterName != null && abi.HasScalarFloatArgument(Base(arg.RegisterName))) { - MarkScalarUse(arg, aliases, scalarArgs); + MarkScalarUse(arg, keys, aliases, scalarArgs); } } } if (writesFloatReturnRegister) { - aliases.Remove("f1"); + aliases[F1AliasKey] = -1; } return; } @@ -892,7 +1045,7 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider foreach (var arg in arguments) { - MarkScalarUse(arg, aliases, scalarArgs); + MarkScalarUse(arg, keys, aliases, scalarArgs); } } @@ -1083,7 +1236,8 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider private static void MarkScalarUse( IrValue value, - IReadOnlyDictionary> aliases, + AliasKeys keys, + int[] aliases, HashSet scalarArgs) { if (value.Kind != "register" || value.RegisterName == null) @@ -1092,24 +1246,22 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider } var baseName = Base(value.RegisterName); - if (!IsFloatRegister(baseName) || !aliases.TryGetValue(baseName, out var sources)) + if (!IsFloatRegister(baseName) || !keys.TryGet(baseName, out var key) || aliases[key] < 0) { return; } - foreach (var source in sources) + for (var sources = aliases[key]; sources != 0; sources &= sources - 1) { - if (AbiFloatArgumentSet.Contains(source)) - { - scalarArgs.Add(source); - } + scalarArgs.Add(AbiFloatArgumentRegisters[BitOperations.TrailingZeroCount(sources)]); } } private static void PropagateFloatAlias( string destination, IrValue value, - Dictionary> aliases) + AliasKeys keys, + int[] aliases) { var destBase = Base(destination); if (!IsFloatRegister(destBase)) @@ -1117,24 +1269,27 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider return; } + keys.TryGet(destBase, out var destKey); if (value.Kind == "register" && value.RegisterName != null && IsFloatRegister(Base(value.RegisterName)) && - aliases.TryGetValue(Base(value.RegisterName), out var srcAliases)) + keys.TryGet(Base(value.RegisterName), out var sourceKey) && + aliases[sourceKey] >= 0) { - aliases[destBase] = new HashSet(srcAliases, StringComparer.OrdinalIgnoreCase); + aliases[destKey] = aliases[sourceKey]; } else { - aliases.Remove(destBase); + aliases[destKey] = -1; } } - private static void KillFloatAlias(string destination, Dictionary> aliases) + private static void KillFloatAlias(string destination, AliasKeys keys, int[] aliases) { - if (!string.IsNullOrWhiteSpace(destination) && IsFloatRegister(Base(destination))) + if (!string.IsNullOrWhiteSpace(destination) && IsFloatRegister(Base(destination)) && + keys.TryGet(Base(destination), out var key)) { - aliases.Remove(Base(destination)); + aliases[key] = -1; } } @@ -1146,70 +1301,6 @@ public sealed class InferredGuestFunctionAbiProvider : IGuestFunctionAbiProvider } } - private static Dictionary> CreateInitialArgumentAliasState() - { - return AbiFloatArgumentRegisters.ToDictionary( - static r => r, - static r => new HashSet(StringComparer.OrdinalIgnoreCase) { r }, - StringComparer.OrdinalIgnoreCase); - } - - private static Dictionary> UnionPredecessorStates( - IReadOnlyList predecessors, - IReadOnlyDictionary>> outMap) - { - var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); - foreach (var pred in predecessors) - { - if (!outMap.TryGetValue(pred, out var predState)) - { - continue; - } - - foreach (var (register, sources) in predState) - { - if (!result.TryGetValue(register, out var destSources)) - { - destSources = new HashSet(StringComparer.OrdinalIgnoreCase); - result[register] = destSources; - } - - destSources.UnionWith(sources); - } - } - - return result; - } - - private static Dictionary> CopyAliasState( - IReadOnlyDictionary> state) - { - return state.ToDictionary( - static kv => kv.Key, - static kv => new HashSet(kv.Value, StringComparer.OrdinalIgnoreCase), - StringComparer.OrdinalIgnoreCase); - } - - private static bool AliasStatesEqual( - IReadOnlyDictionary> left, - IReadOnlyDictionary> right) - { - if (left.Count != right.Count) - { - return false; - } - - foreach (var (key, leftSet) in left) - { - if (!right.TryGetValue(key, out var rightSet) || !leftSet.SetEquals(rightSet)) - { - return false; - } - } - - return true; - } - private static bool IsGuestCallTarget(string target) { if (string.IsNullOrWhiteSpace(target)) diff --git a/translator/src/Translator.Core/Translation/CanonicalIrStore.cs b/translator/src/Translator.Core/Translation/CanonicalIrStore.cs index e47ec2e..a4c57d4 100644 --- a/translator/src/Translator.Core/Translation/CanonicalIrStore.cs +++ b/translator/src/Translator.Core/Translation/CanonicalIrStore.cs @@ -30,6 +30,14 @@ public sealed class CanonicalIrStore : ICanonicalIrProvider } } + public void Remove(uint entryPoint) + { + lock (_gate) + { + _entries.Remove(entryPoint); + } + } + public bool TryGet(uint entryPoint, out IrFunction function) { byte[]? encoded; @@ -55,12 +63,16 @@ internal static class CanonicalIrBinaryCodec IndirectJump, JumpTable, Return, Comment, TracePpc, Undefined } + // Reused per thread so encoding doesn't regrow (and re-fault) a fresh buffer per function. + [ThreadStatic] private static MemoryStream? _encodeStream; + public static byte[] Encode(IrFunction function) { var strings = BuildStringTable(function); var ids = strings.Select((value, index) => (value, index)) .ToDictionary(static pair => pair.value, static pair => pair.index, StringComparer.Ordinal); - using var stream = new MemoryStream(); + var stream = _encodeStream ??= new MemoryStream(); + stream.SetLength(0); using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); writer.Write(Magic); WriteVarUInt(writer, (uint)CanonicalIrStore.FormatVersion); @@ -85,9 +97,11 @@ internal static class CanonicalIrBinaryCodec return stream.ToArray(); } - public static IrFunction Decode(ReadOnlySpan encoded) + public static IrFunction Decode(ReadOnlySpan encoded) => Decode(encoded.ToArray()); + + public static IrFunction Decode(byte[] encoded) { - using var stream = new MemoryStream(encoded.ToArray(), writable: false); + using var stream = new MemoryStream(encoded, writable: false); using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); if (!reader.ReadBytes(Magic.Length).SequenceEqual(Magic) || ReadVarUInt(reader) != CanonicalIrStore.FormatVersion) diff --git a/translator/src/Translator.Core/Translation/GqrInterproceduralAnalysis.cs b/translator/src/Translator.Core/Translation/GqrInterproceduralAnalysis.cs index e8c2c0a..4c44dfe 100644 --- a/translator/src/Translator.Core/Translation/GqrInterproceduralAnalysis.cs +++ b/translator/src/Translator.Core/Translation/GqrInterproceduralAnalysis.cs @@ -497,12 +497,52 @@ public static class GqrInterproceduralAnalysis return masks; } + /// + /// Unknown-entry roots that depend only on the final call graph, never on discovery order: + /// , every candidate without a direct caller, then (ascending) any + /// candidate the roots still don't reach, so no candidate is left without an entry fact source. + /// + public static HashSet SelectUnknownEntryRoots( + IReadOnlyDictionary functions, + IEnumerable fixedRoots, + IEnumerable candidates) + { + var called = new HashSet(); + foreach (var summary in functions.Values) + foreach (var target in summary.DirectGuestTargets) + called.Add(target); + var ordered = candidates.Where(functions.ContainsKey).Distinct().OrderBy(static a => a).ToArray(); + var roots = fixedRoots.Where(functions.ContainsKey).ToHashSet(); + foreach (var address in ordered) + if (!called.Contains(address)) + roots.Add(address); + var reachable = ComputeCompactReachable(functions, roots); + foreach (var address in ordered) + { + if (reachable.Contains(address)) continue; + roots.Add(address); + ExtendCompactReachable(functions, reachable, address); + } + return roots; + } + private static HashSet ComputeCompactReachable( IReadOnlyDictionary functions, IReadOnlySet roots) { var reachable = new HashSet(); - var queue = new Queue(roots); + foreach (var root in roots) + ExtendCompactReachable(functions, reachable, root); + return reachable; + } + + private static void ExtendCompactReachable( + IReadOnlyDictionary functions, + HashSet reachable, + uint root) + { + var queue = new Queue(); + queue.Enqueue(root); while (queue.TryDequeue(out var address)) { if (!reachable.Add(address)) continue; @@ -510,7 +550,6 @@ public static class GqrInterproceduralAnalysis if (functions.ContainsKey(target) && !reachable.Contains(target)) queue.Enqueue(target); } - return reachable; } private static void TransferCompact( diff --git a/translator/tests/Translator.Tests/GqrConstantPropagationTests.cs b/translator/tests/Translator.Tests/GqrConstantPropagationTests.cs index 937dbec..aa36bbf 100644 --- a/translator/tests/Translator.Tests/GqrConstantPropagationTests.cs +++ b/translator/tests/Translator.Tests/GqrConstantPropagationTests.cs @@ -167,6 +167,31 @@ public sealed class GqrConstantPropagationTests instruction => instruction is IrCall { Target: "PPC_PsqLKnown_00070007" }); } + [Fact] + public void UnknownEntryRootsDependOnlyOnTheCallGraph() + { + IrFunction Calls(params uint[] targets) => Function(targets + .Select(target => (IrInstruction)new IrCall(string.Empty, $"0x{target:X8}", Array.Empty())) + .Append(new IrReturn(null)) + .ToArray()); + var functions = new Dictionary + { + [0x80001000] = Calls(), + [0x80002000] = Calls(0x80003000), + [0x80003000] = Calls(), + [0x80004000] = Calls(0x80005000), + [0x80005000] = Calls(0x80004000) + }; + var summaries = functions.ToDictionary(pair => pair.Key, pair => GqrFunctionSummary.Create(pair.Value)); + uint[] candidates = [0x80002000, 0x80003000, 0x80004000, 0x80005000]; + + var roots = GqrInterproceduralAnalysis.SelectUnknownEntryRoots(summaries, [0x80001000u], candidates); + var reversed = GqrInterproceduralAnalysis.SelectUnknownEntryRoots(summaries, [0x80001000u], candidates.Reverse()); + + Assert.Equal(new uint[] { 0x80001000, 0x80002000, 0x80004000 }, roots.Order()); + Assert.Equal(roots.Order(), reversed.Order()); + } + [Fact] public void InterproceduralAnalysisIntersectsConflictingCallers() {