使用Expression-Trees在运行时生成快速排序函数

时间:2019-07-19 22:43:48

标签: c# sorting expression-trees

我有代码可以在运行时使用表达式树生成任何类型的排序函数。下面的示例按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(); 
}

2 个答案:

答案 0 :(得分:1)

  • 首先编写基准测试,以比较您使用Benchmarkdotnet生成的方法与native方法。稍后,您可以定义所做的更改是否使您的代码在性能方面更好
  • 要在表达式树上运行.Compile()很繁重。您应该为特定情况Func<T,T,int>创建一次已编译的委托并将其缓存。

答案 1 :(得分:0)

我编写了基准测试,结果本机与表达式非常接近:

| 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>();
    }
}

这里最重要的是使用设置方法而不是基准测试来编译分类器。