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
.
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
¿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