在C#中优化多重调度通知算法?

发布于 2024-08-23 12:58:48 字数 1492 浏览 4 评论 0原文

抱歉,我想不出更好的方法来描述这个问题。基本上,我正在尝试在游戏中实现碰撞系统。我希望能够注册一个“碰撞处理程序”来处理可以转换为特定类型的两个对象(以任一顺序给出)的任何碰撞。因此,如果 Player : Ship : EntityLaser : Particle : Entity,以及 (Ship, Particle)(Laser, Entity) 的注册比 (Laser, Player) 的碰撞更重要,两个处理程序都应该得到通知,参数的顺序正确,并且 (Laser, Player) 的碰撞Laser) 应该只通知第二个处理程序。

一个代码片段说了一千个单词,所以这就是我现在正在做的事情(天真的方法):

    public IObservable<Collision<T1, T2>> onCollisionsOf<T1, T2>()
        where T1 : Entity
        where T2 : Entity
    {
        Type t1 = typeof(T1);
        Type t2 = typeof(T2);
        Subject<Collision<T1, T2>> obs = new Subject<Collision<T1, T2>>();
        _onCollisionInternal += delegate(Entity obj1, Entity obj2)
        {
            if (t1.IsAssignableFrom(obj1.GetType()) && t2.IsAssignableFrom(obj2.GetType()))
                obs.OnNext(new Collision<T1, T2>((T1) obj1, (T2) obj2));
            else if (t1.IsAssignableFrom(obj2.GetType()) && t2.IsAssignableFrom(obj1.GetType()))
                obs.OnNext(new Collision<T1, T2>((T1) obj2, (T2) obj1));
        };
        return obs;
    }

但是,这个方法非常慢(可测量;实现这个之后我损失了〜2 FPS),所以我正在寻找一种方法从而减少几个周期/分配。

我想到了(例如,花了一个小时实现,然后因为自己是个白痴而把头撞到墙上)一种方法,根据哈希码将类型按顺序排列,然后将它们放入字典中,每个条目都是该类型对的处理程序的链接列表,带有布尔值指示处理程序是否希望反转参数的顺序。不幸的是,这不适用于派生类型,因为如果传入派生类型,它不会通知基类型的订阅者。有谁能想出一种比检查每个类型对(两次)以查看其是否匹配更好的方法吗?

谢谢, 罗伯特

Sorry about the title, I couldn't think of a better way to describe the problem. Basically, I'm trying to implement a collision system in a game. I want to be able to register a "collision handler" that handles any collision of two objects (given in either order) that can be cast to particular types. So if Player : Ship : Entity and Laser : Particle : Entity, and handlers for (Ship, Particle) and (Laser, Entity) are registered than for a collision of (Laser, Player), both handlers should be notified, with the arguments in the correct order, and a collision of (Laser, Laser) should notify only the second handler.

A code snippet says a thousand words, so here's what I'm doing right now (naieve method):

    public IObservable<Collision<T1, T2>> onCollisionsOf<T1, T2>()
        where T1 : Entity
        where T2 : Entity
    {
        Type t1 = typeof(T1);
        Type t2 = typeof(T2);
        Subject<Collision<T1, T2>> obs = new Subject<Collision<T1, T2>>();
        _onCollisionInternal += delegate(Entity obj1, Entity obj2)
        {
            if (t1.IsAssignableFrom(obj1.GetType()) && t2.IsAssignableFrom(obj2.GetType()))
                obs.OnNext(new Collision<T1, T2>((T1) obj1, (T2) obj2));
            else if (t1.IsAssignableFrom(obj2.GetType()) && t2.IsAssignableFrom(obj1.GetType()))
                obs.OnNext(new Collision<T1, T2>((T1) obj2, (T2) obj1));
        };
        return obs;
    }

However, this method is quite slow (measurable; I lost ~2 FPS after implementing this), so I'm looking for a way to shave a couple cycles/allocation off this.

I thought about (as in, spent an hour implementing then slammed my head against a wall for being such an idiot) a method that put the types in an order based on their hash code, then put them into a dictionary, with each entry being a linked list of handlers for pairs of that type with a boolean indication whether the handler wanted the order of arguments reversed. Unfortunately, this doesn't work for derived types, since if a derived type is passed in, it won't notify a subscriber for the base type. Can anyone think of a way better than checking every type pair (twice) to see if it matches?

Thanks,
Robert

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

豆芽 2024-08-30 12:58:48

所有这些反思都不会很快。注意每次碰撞时反射是如何发生的。这就是问题所在。

一些想法。

第一个想法:访客模式。

在 OO 语言中实现相当快速的双虚拟调度的标准方法是通过构建访问者模式的实现。

您能否实现一种访问者模式,使访问者中的每个接受者都维护一个“在这种情况下要做的事情”列表?然后,您的加法器方法包括确定编写新“要做的事情”的正确位置,然后将委托添加到该事情。当冲突发生时,您启动访问者进行双重调度,找到要做的事情的委托,并调用它。

您是否需要两次以上的调度?高效的多虚拟调度是可行的,但并不那么简单。

第二个想法:动态调度

C# 4 具有动态调度。它的工作方式是我们在第一次遇到调用站点时使用反射来分析调用站点。然后,我们动态生成新的 IL 来执行调用,并缓存 IL。在第二次调用时,我们反思参数以查看它们是否与之前的类型完全相同。如果是,我们将重新使用现有的 IL 并调用它。如果没有,我们再进行分析。如果参数通常只有几种类型,那么缓存很快就会开始只有命中而没有未命中,并且考虑到所有因素,性能实际上相当不错。当然比每次都进行大量反思要快。我们每次做的唯一反思就是分析参数的运行时类型。

第三个想法:实现您自己的动态调度

DLR 所做的事情并没有什么神奇之处。它会进行一次分析,吐出一些 IL 并缓存结果。我怀疑你的痛苦正在发生,因为你每次都在重新进行分析。

All that reflection is not going to be fast. Notice how the reflection happens on every collision. That's the problem.

Some thoughts.

Thought number one: visitor pattern.

The standard way to implement reasonably fast double-virtual dispatch in OO languages is via building an implementation of the visitor pattern.

Can you implement a visitor pattern whereby each acceptor in the vistior maintains a list of "things to do in this situation"? Then your adder method consists of identifying the right place to write the new "thing to do", and then add the delegate to that thing. When the collision happens, you start the visitor to do double dispatch, find the delegate of things to do, and invoke it.

Do you need more than double-dispatch ever? Efficient multiple-virtual dispatch is doable but it's not exactly straightforward.

Thought number two: dynamic dispatch

C# 4 has dynamic dispatch. The way it works is we use reflection the first time the call site is encountered to analyze the call site. We then generate fresh new IL dynamically to execute the call, and cache the IL. On the second call, we reflect upon the arguments to see whether they are the exact same types as before. If they are, we re-use the existing IL and just call it. If not, we do the analysis again. If the arguments are typically of only a few types then the cache very quickly starts being only hits and no misses, and performance is actually quite good all things considered. Certainly faster than lots of reflection every single time. The only reflection we do every time is the analysis of the runtime type of the arguments.

Thought number three: implement your own dynamic dispatch

There's nothing magical about what the DLR is doing. It does some analysis once, spits some IL and caches the result. I suspect your pain is happening because you're re-doing the analysis every time.

终止放荡 2024-08-30 12:58:48

如果每个Entity都有一个标识其实体类型的属性,并且您只需获取该属性的值,那不是更快吗?

然后,如果您对处理程序中的实体顺序有约定,则 Laser 将始终位于玩家之前,等等。

您的实体类型可以是 和 枚举,并且枚举顺序可以是您的处理程序对象顺序。

public IObservable<Collision<T1, T2>> onCollisionsOf<T1, T2>()
    where T1 : Entity
    where T2 : Entity
{
    EntityTypeEnum t1 = T1.EntityType;
    EntityTypeEnum t2 = T2.EntityType;

    Subject<Collision<T1, T2>> obs = new Subject<Collision<T1, T2>>();
    _onCollisionInternal += delegate(Entity obj1, Entity obj2)
    {
       if (t1 < t2)
           obs.OnNext(new Collision<T1, T2>((T1) obj1, (T2) obj2));
       else
           obs.OnNext(new Collision<T1, T2>((T1) obj2, (T2) obj1));
    };
    return obs;
}

If each Entity had a property that identified its entity type, and you just fetched the value of that property, wouldn't that be faster?

Then if you had a convention for the order of the entities in your handler, so that Laser would always be before player, etc.

Your entity type could be and enum, and the enum order could be your handler object order.

public IObservable<Collision<T1, T2>> onCollisionsOf<T1, T2>()
    where T1 : Entity
    where T2 : Entity
{
    EntityTypeEnum t1 = T1.EntityType;
    EntityTypeEnum t2 = T2.EntityType;

    Subject<Collision<T1, T2>> obs = new Subject<Collision<T1, T2>>();
    _onCollisionInternal += delegate(Entity obj1, Entity obj2)
    {
       if (t1 < t2)
           obs.OnNext(new Collision<T1, T2>((T1) obj1, (T2) obj2));
       else
           obs.OnNext(new Collision<T1, T2>((T1) obj2, (T2) obj1));
    };
    return obs;
}
空名 2024-08-30 12:58:48

我假设您正在寻找两组碰撞:激光/玩家和激光/激光。如果你愿意让 IObservable<碰撞< T1、T2> >只需匹配这两种情况,然后您就可以将委托减少到只有一项检查,并将所有内容都强类型化以进行比较。

_onCollisionInternal += delegate(T1 obj1, T2 obj2) {
  obs.OnNext(new Collision<T1, T2>( obj1, obj2));
};


public IObservable<Collision<T1, T2>> onCollisionsOf<T1, T2>()
        where T1 : Entity
        where T2 : Entity
    {
        Subject<Collision<T1, T2>> obs = new Subject<Collision<T1, T2>>();
        _onCollisionInternal += delegate(T1 obj1, T2 obj2) {
           obs.OnNext(new Collision<T1, T2>( obj1, obj2));
        };
        return obs;
    }

Observable<Collision<Laser, Player>> lp = onCollisionsOf<Laser, Player>();
Observable<Collision<Laser, Laser>> ll = onCollisionsOf<Laser, Laser>();

I am assuming that there are 2 sets of collisions you are after: laser / player and laser / laser. If you were willing to make IObservable< Collision< T1, T2 > > just match those two cases, then you would be able to reduce the delegate to just one check and have everything strong typed for the comparison.

_onCollisionInternal += delegate(T1 obj1, T2 obj2) {
  obs.OnNext(new Collision<T1, T2>( obj1, obj2));
};


public IObservable<Collision<T1, T2>> onCollisionsOf<T1, T2>()
        where T1 : Entity
        where T2 : Entity
    {
        Subject<Collision<T1, T2>> obs = new Subject<Collision<T1, T2>>();
        _onCollisionInternal += delegate(T1 obj1, T2 obj2) {
           obs.OnNext(new Collision<T1, T2>( obj1, obj2));
        };
        return obs;
    }

Observable<Collision<Laser, Player>> lp = onCollisionsOf<Laser, Player>();
Observable<Collision<Laser, Laser>> ll = onCollisionsOf<Laser, Laser>();
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文