preorden ordenar nodos metodo binario arbol c# .net performance expression-trees

ordenar - nodos c#



Rendimiento de arboles de expresión. (5)

C # 6.0 ahora te permite hacer esto:

public int Add(int x, int y) => x + y;

en lugar de:

public int Add(int x, int y) {return x + y;}

Ver Método Expresiones y Expresiones de Propiedad

Mi entendimiento actual es que el código ''codificado por hardware'' es el siguiente:

public int Add(int x, int y) {return x + y;}

siempre se desempeñará mejor que el código de árbol de expresiones como este:

Expression<Func<int, int, int>> add = (x, y) => x + y; var result = add.Compile()(2, 3); var x = Expression.Parameter(typeof(int)); var y = Expression.Parameter(typeof(int)); return (Expression.Lambda(Expression.Add(x, y), x, y). Compile() as Func<int, int, int>)(2, 3);

ya que el compilador tiene más información y puede dedicar más esfuerzo a optimizar el código si lo compila en tiempo de compilación. ¿Es esto generalmente cierto?


Intentando entender por qué mi compilación y la lambda compilada se ejecutan un poco más lentamente que "solo delegar" (creo que necesitaré crear una nueva pregunta SO). Encontré este hilo y decidí verificar el rendimiento utilizando BenchmarkDotNet. Sorpresa para mi: hay que construirla manualmente y la lambda compilada es la más rápida. Y sí, hay una diferencia estable entre los métodos.

Resultados:

BenchmarkDotNet=v0.10.5, OS=Windows 10.0.14393 Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4 Frequency=3233539 Hz, Resolution=309.2587 ns, Timer=TSC [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1648.0 Clr : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1648.0 Core : .NET Core 4.6.25009.03, 64bit RyuJIT Method | Job | Runtime | Mean | Error | StdDev | Median | Min | Max | Rank | Allocated | --------------- |----- |-------- |----------:|----------:|----------:|----------:|----------:|-----------:|-----:|----------:| AddBuilded | Clr | Clr | 0.8826 ns | 0.0278 ns | 0.0232 ns | 0.8913 ns | 0.8429 ns | 0.9195 ns | 1 | 0 B | AddLambda | Clr | Clr | 1.5077 ns | 0.0226 ns | 0.0212 ns | 1.4986 ns | 1.4769 ns | 1.5395 ns | 2 | 0 B | AddLambdaConst | Clr | Clr | 6.4535 ns | 0.0454 ns | 0.0425 ns | 6.4439 ns | 6.4030 ns | 6.5323 ns | 3 | 0 B | AddBuilded | Core | Core | 0.8993 ns | 0.0249 ns | 0.0233 ns | 0.8908 ns | 0.8777 ns | 0.9506 ns | 1 | 0 B | AddLambda | Core | Core | 1.5105 ns | 0.0241 ns | 0.0201 ns | 1.5094 ns | 1.4731 ns | 1.5396 ns | 2 | 0 B | AddLambdaConst | Core | Core | 9.3849 ns | 0.2237 ns | 0.5693 ns | 9.6577 ns | 8.3455 ns | 10.0590 ns | 4 | 0 B |

No puedo sacar conclusiones de esto, puede ser una diferencia en el código IL o el impacto del compilador JIT.

Código:

static BenchmarkLambdaSimple() { addLambda = (a, b) => a + b; addLambdaConst = AddMethod; var x = Expression.Parameter(typeof(int)); var y = Expression.Parameter(typeof(int)); var additionExpr = Expression.Add(x, y); addBuilded = Expression.Lambda<Func<int, int, int>>( additionExpr, x, y).Compile(); } static Func<int, int, int> addLambda; static Func<int, int, int> addLambdaConst; static Func<int, int, int> addBuilded; private static int AddMethod(int a, int b) { return a + b; } [Benchmark] public int AddBuilded() { return addBuilded(1, 2); } [Benchmark] public int AddLambda() { return addLambda(1, 2); } [Benchmark] public int AddLambdaConst() { return addLambdaConst(1, 2); }


OK, he escrito una pequeña prueba (probablemente necesite un examen por parte de sus expertos) pero parece que los árboles de expresiones son los más rápidos (add3), seguidos de add2 y luego add1.

using System; using System.Diagnostics; using System.Linq.Expressions; namespace ExpressionTreeTest { class Program { static void Main() { Func<int, int, int> add1 = (a, b) => a + b; Func<int, int, int> add2 = AddMethod; var x = Expression.Parameter(typeof(int)); var y = Expression.Parameter(typeof(int)); var additionExpr = Expression.Add(x, y); var add3 = Expression.Lambda<Func<int, int, int>>( additionExpr, x, y).Compile(); TimingTest(add1, "add1", 1000000); TimingTest(add2, "add2", 1000000); TimingTest(add3, "add3", 1000000); } private static void TimingTest(Func<int, int, int> addMethod, string testMethod, int numberOfAdditions) { var sw = new Stopwatch(); sw.Start(); for (var c = 0; c < numberOfAdditions; c++) { addMethod(1, 2); } sw.Stop(); Console.WriteLine("Total calculation time {1}: {0}", sw.Elapsed, testMethod); } private static int AddMethod(int a, int b) { return a + b; } } }

Mi modo de depuración de resultados:

Total calculation time add1: 00:00:00.0134612 Total calculation time add2: 00:00:00.0133916 Total calculation time add3: 00:00:00.0053629

Mi modo de publicación de resultados:

Total calculation time add1: 00:00:00.0026172 Total calculation time add2: 00:00:00.0027046 Total calculation time add3: 00:00:00.0014334


Su función Add probablemente compile a una sobrecarga de la función (si no está en línea) y una única instrucción de adición. No hay nada más rápido que eso.

Incluso la construcción de este árbol de expresiones será más lenta en órdenes de magnitud. Compilar una nueva función para cada invocación será increíblemente costoso en comparación con la implementación directa de C #.

Intenta compilar la función solo una vez y almacenarla en algún lugar.


Compilacion

La llamada a Expression.Compile pasa exactamente por el mismo proceso que cualquier otro código .NET que contenga su aplicación en el sentido de que:

  • Se genera el código IL
  • El código IL está JIT-ted al código de máquina

(El paso de análisis se omite porque ya se creó un Árbol de Expresión y no tiene que generarse desde el código de entrada)

Puede mirar el código fuente del compilador de expresiones para verificar que, efectivamente, se genera el código IL.

Mejoramiento

Tenga en cuenta que casi toda la optimización realizada por el CLR se realiza en el paso JIT, no desde la compilación del código fuente de C #. Esta optimización también se realizará al compilar el código IL desde su delegado lambda a código de máquina.

Tu ejemplo

En tu ejemplo estás comparando manzanas y naranjas. El primer ejemplo es una definición de método, el segundo es un código de tiempo de ejecución que crea un método, lo compila y lo ejecuta. El tiempo que se tarda en crear / compilar el método es mucho más largo que ejecutarlo realmente. Sin embargo, puede mantener una instancia del método compilado después de la creación. Cuando haya hecho eso, el rendimiento de su método generado debe ser idéntico al del método C # original.

Considere este caso:

private static int AddMethod(int a, int b) { return a + b; } Func<int, int, int> add1 = (a, b) => a + b; Func<int, int, int> add2 = AddMethod; var x = Expression.Parameter(typeof (int)); var y = Expression.Parameter(typeof (int)); var additionExpr = Expression.Add(x, y); Func<int, int, int> add3 = Expression.Lambda<Func<int, int, int>>( additionExpr, x, y).Compile(); //the above steps cost a lot of time, relatively. //performance of these three should be identical add1(1, 2); add2(1, 2); add3(1, 2);

Entonces, la conclusión que uno podría sacar es: el código IL es el código IL, sin importar cómo se genere, y las Expresiones Linq generan el código IL.