perf(pathfinder): Optimize pathfind snippets for better performance - #3198
Conversation
…perHackers#3198) PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
…for optimization (TheSuperHackers#3198) The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
7df663c to
c95c628
Compare
This comment was marked as low quality.
This comment was marked as low quality.
This comment was marked as low quality.
This comment was marked as low quality.
…perHackers#3198) PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
…for optimization (TheSuperHackers#3198) The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
c95c628 to
12e8fd4
Compare
|
| Filename | Overview |
|---|---|
| Core/GameEngine/Include/GameLogic/AIPathfind.h | Removes the obsolete PathNode::appendToList declaration after replacing its only use. |
| Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp | Implements constant-time tail appends and reuses previously computed pathfinding values; the previously malformed Path::prependNode definition is now valid. |
Reviews (4): Last reviewed commit: "perf(pathfinder): Remove redundant isCru..." | Re-trigger Greptile
…perHackers#3198) PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
…for optimization (TheSuperHackers#3198) The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
12e8fd4 to
fc87c74
Compare
|
Seems like a good idea to run this against a large number of replays before merging. |
…perHackers#3198) PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
…for optimization (TheSuperHackers#3198) The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
fc87c74 to
65c9a50
Compare
Mauller
left a comment
There was a problem hiding this comment.
Looks good, just needs commit titles updating
…rHackers#3198) PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
…r optimization (TheSuperHackers#3198) The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
65c9a50 to
9e7c160
Compare
Checking now Update 20260915 1541: looks like a mismatch was introduced between 5th and 12th September release in Generals - which hampers running large amount replays for this PR - Currently investigating. ZH is clear (1500 replays, no mismatches) |
|
Replay check complete. No issues by this PR. |
PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
…r optimization (#3198) The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
The generals problem is okay, #3296 is there to help fix one of the mismatch issues. But there is also a mismatch introduced by #2793. |
* bugfix(gamewindow): Remove destroyed windows from the modal stack and prevent duplicate modals for the same window (TheSuperHackers#3224) * feat(commandline): Add working directory command line options (TheSuperHackers#3149) Append -useCwd to apply the startup working directory, -setCwd "path" to apply a custom working directory, otherwise it falls back to the default executable working directory * bugfix(neutronmissile): Fix and improve Nuke Missile damage for large objects inside the outer blast radius (TheSuperHackers#3161) * bugfix(dozeraiupdate): Fix issue where builders could resume completed tasks after being disabled (TheSuperHackers#2793) * refactor(milesaudiomanager): Use consistent variable names for PlayingAudio in MilesAudioManager (TheSuperHackers#3254) * refactor(milesaudiomanager): Simplify MilesAudioManager::notifyOfAudioCompletion() (TheSuperHackers#3254) * refactor(milesaudiomanager): Simplify MilesAudioManager::findLowestPrioritySound() (TheSuperHackers#3254) * bugfix(milesaudiomanager): Fix premature 2d and 3d sound cancellations from MilesAudioManager::stopAudioEvent() (TheSuperHackers#3254) * bugfix(milesaudiomanager): No longer use stopped audio in queries and updates (TheSuperHackers#3254) * refactor(bink): Replace the Bink SDK stub with a Bink runtime loader (TheSuperHackers#3272) The Bink SDK stub was linked as an import library, so binkw32.dll had to be resolvable while the process image was still loading, long before WinMain and therefore long before the command line was parsed. That is why -setCwd could not point a build at a retail installation: the working directory it selects is set far too late to influence how the library is found. BinkLoader loads binkw32.dll explicitly once BinkVideoPlayer is initialized, at which point the working directory is final. The Bink functions declared in bink.h are now ordinary functions that forward to the matching export of the loaded module, so no call site changes. An unresolved function returns the same neutral value the stub library returned, which means a missing binkw32.dll disables video playback instead of preventing the game from starting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(miles): Replace the Miles SDK stub with a Miles runtime loader (TheSuperHackers#3272) The Miles SDK stub was linked as an import library, so mss32.dll had to be resolvable while the process image was still loading, long before WinMain and therefore long before the command line was parsed. That is why -setCwd could not point a build at a retail installation: the working directory it selects is set far too late to influence how the library is found. MilesLoader loads mss32.dll explicitly once the audio device is opened, at which point the working directory is final. The Miles functions declared in mss/mss.h are now ordinary functions that forward to the matching export of the loaded module, so no call site changes. An unresolved function returns the same neutral value the stub library returned, which means a missing mss32.dll turns audio off instead of preventing the game from starting. Nine declarations were dropped along the way, because the retail mss32.dll does not export them and nothing has called them since they were replaced by their volume_pan counterparts: AIL_sample_volume, AIL_set_sample_volume, AIL_sample_pan, AIL_set_sample_pan and the four stream equivalents, plus AIL_open_stream_by_sample. The MSS_auto_cleanup hook was dropped as well, because its atexit handler would have called AIL_shutdown after the module was already freed. All 92 remaining exports were verified to resolve against the retail mss32.dll. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(miles): Fix the written primitive types in mss.h and all its call sites; no ABI changes (TheSuperHackers#3272) * perf(productionupdate): Simplify and correct implementations of cancel functions in ProductionUpdate (TheSuperHackers#3270) * chore(gamememory): Compile out the memory link tester in Release (TheSuperHackers#3266) * perf(gamememory): Early exit delete and free functions on null pointer (TheSuperHackers#3266) * perf(gamememory): Inline preMainInitMemoryManager (TheSuperHackers#3266) * perf(gamememory): Add overloads for the deletes with size_t argument (TheSuperHackers#3266) * chore(gamememory): Remove superfluous extern keywords from operator overloads (TheSuperHackers#3266) * perf(gamememory): Remove unnecessary calls to preMainInitMemoryManager from delete and free functions and make freeBytes noexcept to get rid of EH frame (TheSuperHackers#3266) * fix(gamefont): Ceil font glyph buffer size to the actual glyph size to prevent a buffer write overflow (TheSuperHackers#3268) * refactor(particlesys): Parse IsGroundAligned as an enum instead of a boolean (TheSuperHackers#3265) * ci(release): Stop requesting permissions from the reusable workflow (TheSuperHackers#3276) * refactor(basetype): Add utility functions to Region and Coord types (TheSuperHackers#3271) New functions are: intersectWith, uniteWith for IRegion3D, IRegion2D, Region3D, Region2D updateMin, updateMax for ICoord3D, ICoord2D, Coord3D, Coord2D asICoord2D, asCoord2D for ICoord3D, Coord3D * build(cmake): Add retail compatibility option in CMake config (TheSuperHackers#2379) RTS_BUILD_OPTION_RETAIL_COMPATIBLE_GAME=DEFAULT/ON/OFF * bugfix(meshmatdesc): Fix mesh material color processing (TheSuperHackers#3246) * ci: Restore CI workflow permission compatibility (TheSuperHackers#3286) * chore: Remove trailing commas in braced initializers that break clang-format's compact layout (TheSuperHackers#3274) Scoped to comment-free array/struct literals (BorderColors, TeamGeneric, BezierSegment, GameMemoryInitPools, BFISH, Properties, Scripts) where clang-format explodes each element onto its own line without this. * chore(license): Add SPDX-License-Identifier to LICENSE.md (TheSuperHackers#3290) Helps github detect the license version * bugfix(pathfinder): Restore Generals retail compatibility after crash fix changes to Pathfinder::findAttackPath (TheSuperHackers#3289) * feat(recorder): Play a replay file from the command line (TheSuperHackers#3227) Use -loadreplay <file> as a command line argument to load the replay with full game context * fix(audio): Copy SoundSceneObjClass state safely (TheSuperHackers#3247) * fix(hash): Fix initialization of HashTableIteratorClass and make it work with an empty HashTableClass (TheSuperHackers#3284) * chore(pathfinder): Remove superfluous CPOP_STARTS_FROM_PREV_SEG macro (TheSuperHackers#3295) * fix(milesaudiomanager): Prevent heap-buffer-overflow read in MilesAudioManager::selectProvider() (TheSuperHackers#3281) * bugfix(filesystem): Preserve write paths with missing directories (TheSuperHackers#3104) * perf(pathfinder): Optimize appending node to end of the path (TheSuperHackers#3198) PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else. * perf(pathfinder): Take parents cell's position outside of for-loop for optimization (TheSuperHackers#3198) The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead. * perf(pathfinder): Remove redundant isCrusher recomputation for optimization (TheSuperHackers#3198) * ci(windows): make bink and miles runtime stubs optional in build artifacts * fix(platform): preserve POSIX startup working directory and set Flatpak asset paths * docs(worklog): document CI fixes and verification for upstream sync PR 304 --------- Co-authored-by: ArcticDolphin <5984296+tintinhamans@users.noreply.github.com> Co-authored-by: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Co-authored-by: xezon <4720891+xezon@users.noreply.github.com> Co-authored-by: Stubbjax <11547761+Stubbjax@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: stm <14291421+stephanmeesters@users.noreply.github.com> Co-authored-by: mirelle7 <115191165+mirelle7@users.noreply.github.com> Co-authored-by: Caball009 <82909616+Caball009@users.noreply.github.com> Co-authored-by: Bobby Battista <bobtista@gmail.com> Co-authored-by: SkyAero <21192585+Skyaero42@users.noreply.github.com>
…rHackers#3198) PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
…r optimization (TheSuperHackers#3198) The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
This PR optimizes three code snippets in the pathfinding algorithm to improve its performance. In end-games like FFA's with high number of units, pathfinding can take up to 80% of all CPU time.
For convenience, each snippet optimization is its own commit. PR can be merged by either squash or rebase.
Commits
refactor(pathfind): Optimize appending node to end of the path.
PathNode::appendToList()walks the entire list from the head to find the tail on every call, making repeatedappendNode()calls O(n^2) in path length. Path already tracksm_pathTail, so append directly onto it in O(1) instead. RemovedpathNode::appendToList()as it is not used anywhere else.refactor(pathfind): Take parents cell's position outside of for-loop for optimization.
The parent cell's world position
fromPosnever changes across the neighbour loop, so compute it once instead.refactor(pathfind): Remove redundant isCrusher recomputation for optimization.
The parameter
isCrusheris calculated in theExamineCellsStructand then recalculated in theexamineCellsCallback. By caching the result in theExamineCellStructit reduced the number of evaluations needed.Performance
VS's performance analyser was used.
The appending node commit reduced 3.1% in absolute CPU time for pathfinding.
The parent's cells position and isCrusher optimizations combined reduced 1.2% in absolute CPU time for pathfinding.
Testing
This PR has been tested against 50 normal replays and 1 replay with the pathfind failover activated.
Disclaimer
This PR and its description was fully made by a human.