Skip to content
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

Closed
wants to merge 9 commits into from

Conversation

anvilvapre
Copy link
Contributor

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 avoid MPos allocation in CellLayer.

Additional small changes with minimal performance benefit which I can imagine you won't find 'pretty'. Feel free to reject.

Use PathGraph directly over IGraph 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 copying CellInfo 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 and IPathSearch and pass them as constructor variables i.e. PathQuery as they are often only set once and not used outside the implementing classes.

// Resolve an array index from cell coordinates
int Index(CPos cell)
{
return Index(cell.ToMPos(GridType));
Copy link
Member

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.

Copy link
Contributor Author

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.

@@ -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);
Copy link
Member

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?

@pchote
Copy link
Member

pchote commented Mar 14, 2021

I would support removing IGraph and IPathSearch completely. These were introduced a long time ago by someone who was trying to push for unit testing everywhere, but this did not end well and most things except for these were eventually removed.

Comment on lines 88 to 95
// 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;
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both.

OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs Outdated Show resolved Hide resolved
@@ -15,7 +15,7 @@

namespace OpenRA.Mods.Common.Pathfinder
{
sealed class CellInfoLayerPool
public class CellInfoLayerPool
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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
Copy link
Contributor

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

Comment on lines 88 to 95
// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both.

@anvilvapre
Copy link
Contributor Author

Additional changes in the last push:

As requested I've refactored BasePathSearch, PathSearch from a semi builder pattern - where input and state was spread across multiple classes - to a simpler model.

BasePathSearch and PathGraph now take a PathQuery as constructor argument that defines the search to perform. Leaving only the state to be maintained in BasePathSearch itself. This should allow for alternative implementations more easily in future. PathQuery unfortunately has many fields.

While making changes it seemed to no longer make sense to keep the PathSearch.FromPoints and Search functions. FromPoints became PathQueryType.Position*idirectional and Search PathQueryType.ConditionUnidirectional.

Removed BasePathSearch.StartsPoints as it was not used.

IPathSearch and IPathGraph have been removed as requested. BasePathSearch and PathSearch could now also be optionally merged. Although it may make more sense to move PathSearch.TryExpand to PathGraph rather than BasePathSearch.

Pulled some of the calculation in some CustomCost functions for path searching outside the functions.

Note that by not adding self.Location twice as start point for path searches results in rare conditions in a slightly different path. Which impacts Sync. This is correct.

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).

@@ -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))
Copy link
Contributor

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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Comment on lines 185 to 187
.Where(r => r.Actor != ignore
&& r.Actor.Owner == self.Owner
&& IsAcceptableProcType(r.Actor))
Copy link
Contributor

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.

@anvilvapre anvilvapre force-pushed the 20210302_pathfinder branch 3 times, most recently from 14de522 to 530700b Compare April 7, 2021 21:01
Comment on lines -264 to +265
var layer = a.Layer == 0 ? influence : customInfluence[a.Layer];
var layerIndex = a.Layer;
var layer = layerIndex == 0 ? influence : customInfluence[layerIndex];
Copy link
Contributor

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.

Copy link
Member

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?

Copy link
Member

@pchote pchote left a 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.

@@ -85,6 +85,11 @@ public MPos ToMPos(MapGridType gridType)
return new MPos(u, v);
}

public CPos AtLayer(byte layer)
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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)
Copy link
Member

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);
Copy link
Member

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)
Copy link
Member

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?

Copy link
Contributor Author

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.

@pchote
Copy link
Member

pchote commented Apr 8, 2021

Can you please try to add the changes that use ToLayer, TryPeek, TryPop into the same commits that add them? The value in splitting things into different commits is that we can see each set of changes in isolation and the evolution of the code, rather than have a diff that simply swaps one implementation for another.

@anvilvapre
Copy link
Contributor Author

Can you please try to add the changes that use ToLayer, TryPeek, TryPop into the same commits that add them? The value in splitting things into different commits is that we can see each set of changes in isolation and the evolution of the code, rather than have a diff that simply swaps one implementation for another.

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.

@anvilvapre
Copy link
Contributor Author

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 CPos.WithLayer as a change is not related to PriorityQueue.TryPop. They are to optimize PathGraph and PathSearch independantly.

@pchote
Copy link
Member

pchote commented Apr 8, 2021

Let me one day find the patience to reapply all changes manually across files and turn them into multiple pull requests as well.

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:

  1. Check out a duplicate copy of the branch (e.g. pathfinder-temp
  2. Decide on what subset of changes I want to go in the first commit
  3. Reset all changes that aren't related to those changes (using a graphical tool like gitg makes it a lot easier to unstage individual lines)
  4. Fix any compile errors and test that the subset of features works as intended
  5. Create the first commit
  6. Switch back to the first branch and rebase on the temp branch. Resolve any rebase issues and if needed test anything that you suspect might have regressed.
  7. Repeat steps 1-5 as many times as needed.

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.

@anvilvapre
Copy link
Contributor Author

anvilvapre commented Apr 9, 2021

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.

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: PathQuery, a one liner for skipping 15% of closed cells early and some optimizations for custom goals functions that were passed to path searches.

@AspectInteractive
Copy link
Contributor

AspectInteractive commented Aug 3, 2021

IPathSearch and IPathGraph have been removed as requested.

This seems reasonable since BasePathSearch is the only class that uses these interfaces.

BasePathSearch and PathSearch could now also be optionally merged.

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.

@pchote
Copy link
Member

pchote commented Aug 3, 2021

Activities are currently hardcoding PathSearch directly, so even with all the interfaces it wasn't possible to switch out any implementations. If we want to fix this, this can be done by adding path search wrappers to IPathfinder and changing activities to call these instead.

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.

@AspectInteractive
Copy link
Contributor

AspectInteractive commented Aug 3, 2021

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.

@penev92
Copy link
Member

penev92 commented Aug 10, 2021

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.

@penev92 penev92 marked this pull request as draft August 10, 2021 12:08
@anvilvapre
Copy link
Contributor Author

Closing to reduce number of open PRs. This pull request was split up in many smaller ones. Some merged, some waiting for approval.

@anvilvapre anvilvapre closed this Sep 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants