2011-05-28 22 views
11

Uso tanto C++ como C# y algo que tengo en mente es si es posible usar genéricos en C# para eludir las llamadas a funciones virtuales en las interfaces. Considere lo siguiente:¿Se pueden usar los genéricos Can C# para eludir llamadas a funciones virtuales?

int Foo1(IList<int> list) 
{ 
    int sum = 0; 
    for(int i = 0; i < list.Count; ++i) 
     sum += list[i]; 
    return sum; 
} 

int Foo2<T>(T list) where T : IList<int> 
{ 
    int sum = 0; 
    for(int i = 0; i < list.Count; ++i) 
     sum += list[i]; 
    return sum; 
} 

/*...*/ 
var l = new List<int>(); 
Foo1(l); 
Foo2(l); 

Dentro foo1, cada acceso a list.Count y lista [i] hace una llamada de función virtual. Si esto fuera C++ usando plantillas, entonces en la llamada a Foo2, el compilador podría ver que la llamada a la función virtual puede ser eliminada y encerrada porque el tipo concreto se conoce en el momento de creación de instancias de la plantilla.

Pero, ¿se aplica lo mismo a C# y genéricos? Cuando llama a Foo2 (l), se sabe en tiempo de compilación que T es una lista y, por lo tanto, esa lista. El conteo y la lista [i] no necesitan involucrar llamadas a funciones virtuales. Antes que nada, ¿sería una optimización válida que no rompa algo horriblemente? Y si es así, ¿el compilador/JIT es lo suficientemente inteligente como para hacer esta optimización?

Respuesta

8

Esta es una pregunta interesante, pero desafortunadamente, su enfoque para "engañar" al sistema no mejorará la eficiencia de su programa. Si pudiera, el compilador podría hacerlo por nosotros con relativa facilidad.

Tiene razón en que al llamar al IList<T> a través de una referencia de interfaz, los métodos se envían en tiempo de ejecución y, por lo tanto, no se pueden insertar. Por lo tanto, las llamadas a los métodos IList<T> como Count y el indexador se llamarán a través de la interfaz.

Por otro lado, no es cierto que pueda obtener ninguna ventaja de rendimiento (al menos no con el compilador actual C# y .NET4 CLR), reescribiéndolo como un método genérico.

¿Por qué no? Primero algunos antecedentes.El trabajo de genéricos de C# es que el compilador compila su método genérico que tiene parámetros reemplazables y luego los reemplaza en tiempo de ejecución con los parámetros reales. Esto ya lo sabías

Pero la versión parametrizada del método no sabe más acerca de los tipos de variables que usted y yo en el momento de la compilación. En este caso, todo el compilador sabe acerca de Foo2 es que list es un IList<int>. Tenemos la misma información en el genérico Foo2 que hacemos en el Foo1 no genérico.

Como cuestión de hecho, a fin de evitar código-hinchazón, el compilador JIT solamente produce un solo instanciación del método genérico para todos los tipos de referencia. Aquí está el Microsoft documentation que describe esta sustitución y creación de instancias:

Si el cliente especifica un tipo de referencia, entonces el compilador JIT reemplaza los parámetros genéricos en el servidor con IL objeto, y lo compila en código nativo. Ese código se usará en cualquier solicitud adicional para un tipo de referencia en lugar de un parámetro de tipo genérico. Tenga en cuenta que de esta forma el compilador JIT solo reutiliza el código real. Las instancias todavía se asignan según su tamaño fuera del montón administrado, y no hay conversión.

Esto significa que la versión del compilador JIT del método (para los tipos de referencia) es No escriba segura pero no importa porque el compilador ha asegurado toda seguridad de tipos en tiempo de compilación. Pero, lo que es más importante para su pregunta, no hay ninguna forma de realizar la alineación y obtener un impulso en el rendimiento.

Editar: Por último, empíricamente, acabo de hacer un punto de referencia tanto de Foo1 y Foo2 y con ellos se obtienen los resultados de rendimiento idénticas. En otras palabras, Foo2 es no más rápido que Foo1.

Vamos a añadir un "inlinable" versión Foo0 para la comparación:

int Foo0(List<int> list) 
{ 
    int sum = 0; 
    for (int i = 0; i < list.Count; ++i) 
     sum += list[i]; 
    return sum; 
} 

Aquí está la comparación de rendimiento:

Foo0 = 1719 
Foo1 = 7299 
Foo2 = 7472 
Foo0 = 1671 
Foo1 = 7470 
Foo2 = 7756 

Así se puede ver que Foo0, que puede ser inline, es mucho más rápido que los otros dos También puede ver que Foo2 es un poco más lento en lugar de estar tan cerca como Foo0.

+0

Muy agradable. Mirando el IL para 'Foo1' y' Foo0' en LINQPad muestra las diferencias muy bien. En 'Foo1', las operaciones' callvirt' muestran llamadas a 'IList get_Item' y' ICollection .get_Count', mientras que las '' callvirt' ops 'de' Foo0' son 'List .get_Item' y 'List .get_Count'. La lentitud de 'Foo2' puede explicarse por la necesidad de ejecutar' ldarga.s' y luego anteponer 'callvirt' con' constrained' en lugar de simplemente ejecutar 'ldarg.1'. – arcain

+1

¿Esto no significa que puede engañar al sistema haciendo que las estructuras implementen su interfaz, y luego usar esas estructuras en su método/clase genérica? Como son tipos de valor, el tiempo de ejecución creará múltiples implementaciones concretas para cada tipo de estructura ... – Cesar

4

Esto realmente funciona, y si (si la función no es virtual) da como resultado una llamada no virtual. La razón es que, a diferencia de C++, los genéricos de CLR definen, en JIT time, una clase concreta específica para cada conjunto único de parámetros genéricos (indicado por reflexión a través de 1, 2, etc.). Si el método es virtual, dará como resultado una llamada virtual como cualquier método concreto, no virtual, no genérico.

Lo que hay que recordar acerca de los genéricos .NET es que dado:

Foo<T>; 

continuación

Foo<Int32> 

es un tipo válido en tiempo de ejecución, separada y distinta de

Foo<String> 

, y todos los métodos virtuales y no virtuales se tratan en consecuencia. Esta es la razón por la cual se puede crear un

List<Vehicle> 

y añadir un coche a ella, pero no se puede crear una variable de tipo

List<Vehicle> 

y establezca su valor a una instancia de

List<Car> 

. Son de tipos diferentes, pero el primero tiene un método Add(...) que toma un argumento de Vehicle, un supertipo de Car.

+1

No estoy seguro de hasta qué punto se pueden evitar las llamadas a funciones virtuales, pero en los casos en que una estructura puede implementar una interfaz, se puede eliminar el box pasando la estructura como un parámetro genérico que está restringido a una interfaz de pasarlo como una interfaz. – supercat

Cuestiones relacionadas