New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Path finding optimizations. Skip (15%) closed cells early. #19245
Conversation
// Resolve an array index from cell coordinates | ||
int Index(CPos cell) | ||
{ | ||
return Index(cell.ToMPos(GridType)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we do here what you currently do in ToOffset
? I'm not sure if we want to expose that ToOffset
logical to everything when it is only used here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I first implemented it there. But then it seemed cleaner to keep all ToMPos
logic in CPos
- not spread duplicated code. ToOffset
also seemed generic enough. I don't mind where.
OpenRA.Game/CPos.cs
Outdated
@@ -42,6 +42,7 @@ public CPos(int x, int y, byte layer) | |||
} | |||
|
|||
public static readonly CPos Zero = new CPos(0, 0, 0); | |||
public static readonly CPos Invalid = new CPos(-2048, -2048, 255); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using magic values on CPos has caused many headaches in the past. Could we instead change CPos IPathSearch.Expand
to bool IPathSearch.TryExpand(out CPos)
and avoid this completely?
I would support removing |
84b9cf9
to
cef16f0
Compare
// PERF: Use fields over virtual properties | ||
readonly Actor actor; | ||
readonly World world; | ||
Func<CPos, bool> customBlock; | ||
Func<CPos, int> customCost; | ||
int laneBias; | ||
bool inReverse; | ||
Actor ignoreActor; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some testing and it looks like the jit will inline these correctly.
using System;
public interface Apa
{
int Prop { get; }
}
public class C : Apa {
public int Prop { get; } = 10;
private int field = 10;
public int PropAccess() {
var x = Prop + 10;
return x;
}
public int FieldAccess() {
var x = field + 10;
return x;
}
}
; Core CLR v5.0.421.11614 on x86
C..ctor()
L0000: mov dword ptr [ecx+4], 0xa
L0007: mov dword ptr [ecx+8], 0xa
L000e: ret
C.get_Prop()
L0000: mov eax, [ecx+4]
L0003: ret
C.PropAccess()
L0000: mov eax, [ecx+4]
L0003: add eax, 0xa
L0006: ret
C.FieldAccess()
L0000: mov eax, [ecx+8]
L0003: add eax, 0xa
L0006: ret
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also in class `class D : C'? Also when there is a setter? Because with a setter i can imagine it no longer 'dares' to?
I read somewhere C# wouldn't/couldn't. Good to know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In both those cases its able to inline it proper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So shall I, some time later, remove both interfaces and change the fields back to properties? Or only the last.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both.
@@ -15,7 +15,7 @@ | |||
|
|||
namespace OpenRA.Mods.Common.Pathfinder | |||
{ | |||
sealed class CellInfoLayerPool | |||
public class CellInfoLayerPool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
think it best to keep it sealed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not possible as Graph is public that is used by a PathSearch which is also public.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public sealed
UnitPathToRange | ||
} | ||
|
||
public struct PathCacheKey |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be readonly othewise a copy will be made when using the in param, see https://docs.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code
// PERF: Use fields over virtual properties | ||
readonly Actor actor; | ||
readonly World world; | ||
Func<CPos, bool> customBlock; | ||
Func<CPos, int> customCost; | ||
int laneBias; | ||
bool inReverse; | ||
Actor ignoreActor; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both.
cef16f0
to
b0d0c6a
Compare
Additional changes in the last push: As requested I've refactored
While making changes it seemed to no longer make sense to keep the Removed
Pulled some of the calculation in some Note that by not adding No functional changes (intended). Apart from the impact on sync mentioned above. Tested by running a 7 minute replay that was recorded on bleed (with the one change that impacts sync temporary disabled). |
b0d0c6a
to
a47816f
Compare
@@ -162,12 +162,14 @@ public override bool Tick(Actor self) | |||
// Harvesters should respect an explicit harvest order instead of harvesting the current cell. | |||
if (orderLocation == null) | |||
{ | |||
if (harv.CanHarvestCell(self, self.Location) && claimLayer.CanClaimCell(self, self.Location)) | |||
if (harv.CanHarvestCell(self, self.Location) | |||
&& claimLayer.CanClaimCell(self, self.Location)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only formatting changes?
return self.Location; | ||
} | ||
else | ||
{ | ||
if (harv.CanHarvestCell(self, orderLocation.Value) && claimLayer.CanClaimCell(self, orderLocation.Value)) | ||
if (harv.CanHarvestCell(self, orderLocation.Value) | ||
&& claimLayer.CanClaimCell(self, orderLocation.Value)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
.Where(r => r.Actor != ignore | ||
&& r.Actor.Owner == self.Owner | ||
&& IsAcceptableProcType(r.Actor)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please keep this in one line.
14de522
to
530700b
Compare
var layer = a.Layer == 0 ? influence : customInfluence[a.Layer]; | ||
var layerIndex = a.Layer; | ||
var layer = layerIndex == 0 ? influence : customInfluence[layerIndex]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fun fact: It did look like the jit compiler was able to do this optimization by itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we drop these then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of small comments i noticed while skimming the commit below.
My overall feedback is that this is trying to do way too many different things within a single commit. This makes it very difficult to understand how the different changes relate to eachother and overall difficult to review. Can you please split this up into individual commits (they can all still be in this PR) for each logicial change?
e.g. introducing PathQuery
should have its own commit that switches the code to the new class without changing any behaviour, the harvester pathfinding improvements should have another, the different micro-optimizations each get their own, and so on.
OpenRA.Game/CPos.cs
Outdated
@@ -85,6 +85,11 @@ public MPos ToMPos(MapGridType gridType) | |||
return new MPos(u, v); | |||
} | |||
|
|||
public CPos AtLayer(byte layer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we please call this ToLayer
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. But it will look similar to ToMPos
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, thats a good point. How about WithLayer
then.
AtLayer
sounds like it should be returning true/false if the given layer matches...
|
||
while (addLevel >= 1 && comparer.Compare(Above(addLevel, addIndex), item) > 0) | ||
while (addLevel >= 1 && comparer.Compare((above = Above(addLevel, addIndex)), item) > 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We generally try to avoid using inline assignments (not sure the correct term) like this. Can we restructure the loop so that above is assigned on its own line?
{ | ||
if (level == 0) | ||
{ | ||
item = default(T); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can now be just item = default;
, likewise in TryPop
.
|
||
public bool TryPop(out T item) | ||
{ | ||
if (level == 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pop
also checks index
. Is it safe to ignore that here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty
only uses level
. So I assumed it would be safe.
530700b
to
a2c9125
Compare
Can you please try to add the changes that use |
8a2222c
to
e70198a
Compare
I basically can't. I rebased everything quite often because I thought you wanted it. I made changes before and after committing PathQuery and they spread multiple files. After i renamed PathQuery where it first had a different name. Evolution took side paths ; ) I agree PathQuery could have better been in a separate pull request. Should have suggested this. Let me one day find the patience to reapply all changes manually across files and turn them into multiple pull requests as well. |
Current split up seems the most fine grained I can make it without having to rewrite code and unravel multiple changes per file even per line. Started with a mergetool but it really means rewriting all changes manually. Note that |
This shouldn't be necessary! Here is the workflow I usually use in this situation, which is faster in practice than my list might imply:
A lot of the changes end up being obviously distinct, which means that you can make several commits in a single pass. Its only the core parts where different features overlap where the full loop is needed. |
e70198a
to
8963ac3
Compare
It was the process that i started. I've extracted the distinct. Because I changed a lot of class field members I can no longer easily pick distinct changes without breaking the code. It means that the last commit now contains three changes: |
This seems reasonable since BasePathSearch is the only class that uses these interfaces.
I am quite new and not that familiar with the code base, but would this not prevent alternative PathSearch implementations from being used? I can understand removing the interface, as it has many implementation details that are unlikely to be used by a second class, however BasePathSearch appears general enough - or at least should be - to take several implementations. PathSearch directly implements the A* Algorithm, meaning all pathfinding would need to use this A* algorithm from this point onwards. Given that pathfinding is still not an entirely solved problem, I don't think hardcoding the A* algorithm here (or any specific pathfinding algorithm) is a wise choice. |
Activities are currently hardcoding I'm not convinced we win much by fixing this, however, since these activities all make strong assumptions about the cell and pathfinding system and likely need to be completely replaced by any mod that wants to change the way the grid movement system works. |
There are still other grid-based path finders you may wish to use in the future. A basic implementation of the flow field algorithm for instance is compatible with grid-based movement. For this reason I agree with your initial point of fixing the iPathFinder interface (and eventually decoupling Activities) rather than removing it entirely. |
I believe this is the original/base PR that is getting split over multiple others with the intent of getting closed in the end so to mitigate (my own) confusion I'll convert this into a Draft PR for the time being. |
Closing to reduce number of open PRs. This pull request was split up in many smaller ones. Some merged, some waiting for approval. |
In path finding
GetConnections
considers connections to already closed cells and calculates the cost for them. The caller afterwards ignores them. These are 15% of all connections.Add
CPos.ToOffset
to avoidMPos
allocation inCellLayer
.Additional small changes with minimal performance benefit which I can imagine you won't find 'pretty'. Feel free to reject.
Use
PathGraph
directly overIGraph
to allow non virtual index operator access. There will likely be no alternative implementations - and switching back is easy once there are.Copies
CellInfo
to a local variable. The trade off between copyingCellInfo
versus calculating a cell index offset - and accessing an array twice.Tested by running a replay of 20 mins.
Suggestion: remove most properties in
IGraph
andIPathSearch
and pass them as constructor variables i.e.PathQuery
as they are often only set once and not used outside the implementing classes.