我们能获得代表的身份吗?

时间:2015-10-05 07:55:24

标签: c# .net delegates

是否可以生成委托的标识以区别于其他委托?想想这段代码:

Func<int, int, int> delegate1 = a, b => a + b;
Func<int, int, int> delegate2 = a, b => a + b;
Func<int, int, int> delegate3 = a, b => a - b;
let id1 = id(delegate1);
let id2 = id(delegate2);
let id3 = id(delegate3);
Assert(id1 == id2);
Assert(id1 != id3);

我想解决的问题是,我想在.NET中缓存一些JIT编译的GPU代码。为了方便使用,我想让用户发送委托,如果委托相同,我们会尝试从缓存中找出GPU代码,而不是每次JIT编译它:

Parallel(outputA, inputA1, inputA2, a, b => a + b); //should JIT compile GPU code, and cache it by its identity
Parallel(outputB, inputB1, inputB2, a, b => a + b); //should NOT JIT compile GPU code, and use the cached GPU code by its identity

一种可能的解决方案是比较表达式字符串,但仍然有问题赶上clouser,例如:

int c = 0;
Expression<Func<int, int, int>> delegate1 = (a, b) => a + b + c;
c += 1;
Expression<Func<int, int, int>> delegate2 = (a, b) => a + b + c;
Expression<Func<int, int, int>> delegate3 = (a, b) => a - b - c;
Console.WriteLine(delegate1);
Console.WriteLine(delegate2);
Console.WriteLine(delegate1.ToString() == delegate2.ToString());
Console.ReadKey();

感谢@SWeko和@Luaan,在上面的示例中,delegate1delegate2实际上是相同的。但缓存委托的目的有以下几种用法:

int c = 1;
Parallel(outputA, inputA1, inputA2, (a,b) => a+b); //do JIT compile of GPU code
c += 1;
Parallel(outputB, inputB1, inputB2, (a,b) => a+b); //as the delegate is same then the previouse one, it will not do JIT GPU code compiling, but then, that is wrong!

3 个答案:

答案 0 :(得分:4)

一种(相对天真的)方法是使用Expression<Func>而不是Func自己,因为它们有更详细的信息,允许您分析内容。 E.g。

Expression<Func<int, int, int>> delegate1 = (a, b) => a + b;
Expression<Func<int, int, int>> delegate2 = (a, b) => a + b;
Expression<Func<int, int, int>> delegate3 = (a, b) => a - b;
var id1 = id(delegate1);
var id2 = id(delegate2);
var id3 = id(delegate3);
Debug.Assert(id1 == id2);
Debug.Assert(id1 != id3);

其中id与以下内容一样简单:

public static string id(Expression<Func<int, int, int>> expression)
{
    return expression.ToString();
}

通过测试。

请注意,这不是一个完整的解决方案,并且存在很多问题。如果需要进行全面比较,则需要考虑表达式的完整性,包括(但不限于)进出表达式,方法调用,闭包访问等等的类型。 在一般情况下,我认为这根本不可解决,但通常可以限制在一些更专业的情况下,可以解决。

答案 1 :(得分:3)

您需要在Expression级别工作。 C#编译器不保证相同的lambda最终具有相同的委托对象。此优化目前尚未执行,但存在GitHub问题。即使它被执行,它也会一次完成一个装配,这对你来说可能是不够的。如果委托捕获了闭包值,那么这将无法工作。

我曾经这样做是为了在给定查询的情况下自动缓存LINQ 2 SQL编译查询。比较表达式树并非易事。 ToString并非完全保真,而且速度很慢。您需要编写自己的比较器类。我认为网上有代码作为起点。

例如,当您比较常量表达式时,您不能只执行ReferenceEquals(val1, val2)。实际上,您必须使用多种类型的特殊情况,例如盒装基元类型。它们可以具有相同的值,但装有不同的对象标识。

您还需要编写哈希码函数,因为您可能希望使用哈希表来查找缓存结果。

您还会遇到GC问题,因为表达式树可以保留在任意对象上。基本上,闭包可以以意想不到的方式随机保留局部变量。所以我所做的就是在缓存树木之前对它们进行消毒。我删除了所有不安全的常量。

  

是否可以生成委托的身份以区别于其他委托?

是的,但它涉及手动比较和散列表达式树。

答案 2 :(得分:2)

这里的一个选项是使用Dictionary<T1, T2>来保存字符串ID和我们的委托(字符串Key包含与某些测试调用的值连接的已排序表达式[这会生成某种类型的UID])并且value是我们的Func委托。当我们将转换为字符串的表达式映射到_delegatesMapping中的ID时,表达式仅首次编译:

public class Funker
{
    private static Dictionary<string, string> _delegatesMapping;
    private static Dictionary<string, Func<int, int, int>> _delegates;
    public static Funker Instance { get; private set; }

    static Funker()
    {
        _delegatesMapping = new Dictionary<string, string>();
        _delegates = new Dictionary<string, Func<int, int, int>>();
        Instance = new Funker();
    }

    private Funker() { }

    public Func<int, int, int> this[Expression<Func<int, int, int>> del]
    {
        get
        {
            string expStr = del.ToString();

            var parameters = del.Parameters;

            for (int i = 0; i < parameters.Count; i++)
                expStr = expStr.Replace(parameters[i].Name, String.Concat('_' + i));

            Func<int, int, int> _Del = null;

            if (!_delegatesMapping.ContainsKey(expStr))
            {
                _Del = del.Compile();
                _delegatesMapping.Add(expStr, new String(expStr.OrderBy(ch => ch).ToArray()) + "|" + _Del(55, 77));
            }

            if (!_delegates.ContainsKey(_delegatesMapping[expStr])) _delegates.Add(_delegatesMapping[expStr], _Del ?? del.Compile());
            return _delegates[_delegatesMapping[expStr]];
        }
    }
}

我们需要这个_delegatesMapping以表达式字符串格式存储我们的UID记录 - &gt; UID。它还允许我们执行快速(几乎n(1))查找,而不是在每次需要时计算UID。

所以整个操作看起来像:

表达式 - &gt; textExp - &gt; _delegatesMapping中的新记录(textExp - &gt; UID) _delegates[UID]

中的新记录

当它一直回到它的时候。首先我们获得UID而不是委托:

表达式 - &gt; textExp - &gt; _delegates [_delegatesMapping [textExp]](如果字典中存在则返回委托)。

使用示例

class Program
{        

    static void Main(string[] args)
    {
        var funker = Funker.Instance;

        var del1 = funker[(a, b) => a + 71 + 12 + b];
        var del2 = funker[(hello, world) => 71 + hello + world + 12];
        var del3 = funker[(a, c) => a + 17 + 21 + c];

        Debug.Assert(del1 == del2);
        Debug.Assert(del1 == del3);
    }
}

输出:

true
false

P.S。 但正如SWeko在他的回答中写道的那样:

  

请注意,这不是一个完整的解决方案,并且有很多很多   的问题。如果您需要全面比较,您需要采取   考虑到表达的全部性质,包括(但不是   进入和退出表达式,方法调用,   关闭访问等等。