【问题标题】:Generating a fast sort function at runtime using Expression-Trees使用表达式树在运行时生成快速排序函数
【发布时间】:2019-07-19 22:43:48
【问题描述】:

我有代码可以在运行时使用表达式树为任何类型生成排序函数。下面的示例按 public int 或 string 属性排序,但很容易扩展以包含其他类型的属性。生成的排序函数比手工编写的等效函数慢大约 4 倍。使用 benchmarkdotnet 比较了执行时间与手动编码版本。如何更改生成代码以生成更快的排序函数?

public class SortBy
{
    public bool Ascending { get; set; }
    public string PropName {get; set;}
}

public static class SortFuncCompiler
{
    private static readonly MethodInfo _strCompareTo = typeof(string).GetMethod("CompareTo", new[] {typeof(string)});
    private static readonly MethodInfo _intCompareTo = typeof(int).GetMethod("CompareTo", new[] {typeof(int)});

    public static Func<T,T,int> MakeSortFunc<T>(IList<SortBy> sortDescriptors)
    {
        ParameterExpression param1Expr = Expression.Parameter(typeof(T));
        ParameterExpression param2Expr = Expression.Parameter(typeof(T));
        BlockExpression exprSd = MakeCompositeCompare(param1Expr, param2Expr, sortDescriptors);
        Expression<Func<T,T,int>> lambda = Expression.Lambda<Func<T,T,int>>(exprSd, param1Expr, param2Expr);
        return lambda.Compile();
    }

    private static BlockExpression MakePropertyCompareBlock(
        SortBy sortDescriptor, 
        ParameterExpression rm1, 
        ParameterExpression rm2, 
        LabelTarget labelReturn,
        ParameterExpression result)
    {
        try
        {
            MemberExpression propA = Expression.Property(rm1, sortDescriptor.PropName);
            MemberExpression propB = Expression.Property(rm2, sortDescriptor.PropName);
            var (prop1, prop2) = sortDescriptor.Ascending ? (propA, propB) : (propB, propA);

            Expression compareExpr;

            if(prop1.Type == typeof(string))
            {
                compareExpr = Expression.Call(prop1, _strCompareTo, prop2);
            }
            else if(prop1.Type == typeof(int))
            {
                compareExpr = Expression.Call(prop1, _intCompareTo, prop2);
            }
            else
            {
                throw new ApplicationException($"unsupported property type: {prop1.Type}");
            }

            IEnumerable<ParameterExpression> variables = new[] {result};

            IEnumerable<Expression> expressions = new Expression[]
            {
                Expression.Assign(result, compareExpr),
                Expression.IfThen(
                    Expression.NotEqual(Expression.Constant(0), result),
                    Expression.Goto(labelReturn, result))
            };

            return Expression.Block(variables, expressions);
        }
        catch
        {
            throw new ApplicationException($"unknown property: {sortDescriptor.PropName}");
        }       
    }

    private static BlockExpression MakeCompositeCompare(ParameterExpression param1Expr, ParameterExpression param2Expr, IEnumerable<SortBy> sortBys )
    {
        ParameterExpression result = Expression.Variable(typeof(int), "result");
        LabelTarget labelReturn = Expression.Label(typeof(int));
        LabelExpression labelExpression = Expression.Label(labelReturn, result);
        IEnumerable<Expression> compareBlocks = sortBys.Select(propName => MakePropertyCompareBlock(propName, param1Expr, param2Expr, labelReturn, result));
        return Expression.Block(new[] {result}, compareBlocks.Append(labelExpression));         
    }

}

如何使用生成的排序函数

public class MyComparer<T> : IComparer<T>
{
    private Func<T, T, int> _sortFunc;

    public MyComparer(Func<T, T, int> sortFunc)
    {
        _sortFunc = sortFunc;
    }

    public int Compare(T x, T y) => _sortFunc(x, y);
}


//the expression-tree generated sorting function should be of form
static int SortOneIntOneStrHC(MyClass aa, MyClass bb)
{
    int s1 = aa.IntProp1.CompareTo(bb.IntProp1);

    if (s1 != 0) return s1;

    // aa and bb flipped, as this comparison is descending
    return bb.StrProp1.CompareTo(aa.StrProp1);
}


public class MyClass
{
    public int IntProp1 { get; set; }
    public int IntProp2 { get; set; }
    public string StrProp1 { get; set; }
    public string StrProp2 { get; set; }
}


void Main()
{
    var xs = new List<MyClass>
    {
        new MyClass{IntProp1 = 99, IntProp2 = 88, StrProp1 = "aa", StrProp2 ="bb"},
        new MyClass{IntProp1 = 11, IntProp2 = 22, StrProp1 = "xx", StrProp2 ="yy"},
        new MyClass{IntProp1 = 11, IntProp2 = 22, StrProp1 = "pp", StrProp2 ="qq"},
    };

    var sortBys = new List<SortBy>
    {
        new SortBy{PropName = "IntProp2", Ascending = true},
        new SortBy{PropName = "StrProp1", Ascending = false}
    };

    Func<MyClass, MyClass, int> sortMyClass = SortFuncCompiler.MakeSortFunc<MyClass>(sortBys);

    var ys = xs.OrderBy(x => x, new MyComparer<MyClass>(sortMyClass));

    ys.Dump(); 
}

【问题讨论】:

  • 嗨,你能举个例子来说明我们正在比较什么吗?即“手写”的东西 - 是手写 IComparer&lt;T&gt; 实现还是 OrderBy / ThenBy 链?
  • 嗨,Ivan,我的意思是像上面的 SortOneIntOneStrHC 这样的表单函数,它被传递给 MyComparer ctor 以创建比较器
  • 使用 CompileToMethod,正如您所指出的问题所建议的那样,使表达式树生成方法的性能非常接近手工编码的 C#。例如在我的一项基准测试中,生成的方法需要 22.2 毫秒,手动编码的 C# 需要 20.6 毫秒。谢谢
  • 使用我的函数的 CompileToMethod 版本,我现在得到 System.MethodAccessExeceptions,任何关于如何修复的想法都将不胜感激。

标签: c# sorting expression-trees


【解决方案1】:
  • 首先编写基准测试,将生成的方法与使用Benchmarkdotnetnative 方法进行比较。稍后您可以定义您的更改是否会使您的代码在性能方面更好
  • 在表达式树上运行.Compile() 非常繁重。您应该为特定情况创建一次编译委托 Func&lt;T,T,int&gt; 并缓存它。

【讨论】:

  • 我已经在缓存生成的方法,我的问题是生成方法的执行时间。
【解决方案2】:

我做了编写的基准测试,结果非常接近原生 vs 表达式:

| Method |     N |       Mean |      Error |     StdDev |     Median |
|------- |------ |-----------:|-----------:|-----------:|-----------:|
|   Linq |  1000 |   196.6 us |   3.431 us |   3.209 us |   197.5 us |
| Native |  1000 |   215.8 us |   8.043 us |  21.881 us |   208.0 us |
|   Linq | 10000 | 3,094.8 us | 108.795 us | 306.857 us | 2,991.8 us |
| Native | 10000 | 3,078.2 us | 104.824 us | 107.647 us | 3,056.5 us |

也许您的基准代码本身存在一些错误。这是我使用的:

public class Benchmark
{
    private MyClass[] data;

    private MyComparer<MyClass> linqComparer;

    private MyComparer<MyClass> nativeComparer;

    [Params(1000, 10000)]
    public int N;

    [GlobalSetup]
    public void Setup()
    {
        var random = new Random();

        data = new MyClass[N];
        for (int i = 0; i < N; ++i)
        {
            data[i] = MyClass.Generate(random, N*10, 25);
        }

        // Compile order methods
        List<SortBy> sortBys = new List<SortBy>
        {
            new SortBy{PropName = "IntProp2", Ascending = true},
            new SortBy{PropName = "StrProp1", Ascending = false}
        };
        Func<MyClass, MyClass, int> sortMyClass = SortFuncCompiler.MakeSortFunc<MyClass>(sortBys);

        linqComparer = new MyComparer<MyClass>(sortMyClass);
        nativeComparer = new MyComparer<MyClass>(Sorters.SortOneIntOneStrHC);
    }

    [Benchmark]
    public MyClass[] Linq()
    {
        return data.OrderBy(x => x, linqComparer).ToArray();
    }

    [Benchmark]
    public MyClass[] Native()
    {
        return data.OrderBy(x => x, nativeComparer).ToArray();
    }
}

public class Program
{
    public static void Main()
    {
        var summary = BenchmarkRunner.Run<Benchmark>();
    }
}

这里最重要的是在 setup 方法中编译 sorter,而不是在 benchmark 中。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-11
  • 1970-01-01
  • 1970-01-01
  • 2014-09-17
  • 1970-01-01
  • 2019-03-10
相关资源
最近更新 更多