polimorfismo - llamar metodo de clase abstracta c#
Implementación CLR de llamadas a métodos virtuales para miembros de la interfaz (3)
Por curiosidad: ¿cómo desplaza el CLR las llamadas de métodos virtuales a los miembros de la interfaz para implementarlos correctamente?
Conozco la VTable que mantiene el CLR para cada tipo con ranuras de método para cada método, y el hecho de que para cada interfaz tiene una lista adicional de ranuras de método que apuntan a las implementaciones del método de interfaz asociada. Pero no entiendo lo siguiente: ¿cómo determina el CLR de manera eficiente qué lista de ranuras de método de interfaz se debe elegir de la tabla de tabla del VTable?
El artículo Drill Into .NET Framework Internals para ver cómo CLR crea objetos de tiempo de ejecución de la edición de mayo de 2005 de MSDN Magazine habla sobre una tabla de mapeo de nivel de proceso IVMap indexada por ID de interfaz. ¿Significa esto que todos los tipos en el mismo proceso tienen el mismo puntero al mismo IVMap?
También establece que:
Si
MyInterface1
se implementa mediante dos clases, habrá dos entradas en la tabla IVMap. La entrada apuntará al comienzo de la sub-tabla incrustada dentro de la tabla del métodoMyClass
.
¿Cómo sabe el CLR qué entrada elegir? ¿Hace una búsqueda lineal para encontrar la entrada que coincida con el tipo actual? ¿O una búsqueda binaria? ¿O algún tipo de indexación directa y tener un mapa con posiblemente muchas entradas vacías en él?
También he leído el capítulo sobre Interfaces en CLR a través de la tercera edición de C #, pero no se habla de esto. Por lo tanto, las respuestas a esta otra pregunta no responden a mi pregunta.
Desde el primer artículo que vinculó:
Si MyInterface1 se implementa mediante dos clases, habrá dos entradas en la tabla IVMap. La entrada apuntará al inicio de la sub-tabla incorporada dentro de la tabla del método MyClass, como se muestra en la Figura 9
y
El ClassLoader recorre los metadatos de la clase actual, la clase principal y las interfaces, y crea la tabla de métodos. En el proceso de diseño, reemplaza cualquier método virtual anulado, reemplaza cualquier método de clase principal que se oculte, crea nuevas ranuras y duplica las ranuras según sea necesario. La duplicación de ranuras es necesaria para crear la ilusión de que cada interfaz tiene su propia mini vtable. Sin embargo, las ranuras duplicadas apuntan a la misma implementación física.
Esto me sugiere que el IVMap de la interfaz tiene entradas marcadas por el nombre de la clase (o algún equivalente) que apunta a una subsección del vtable de la clase, que esencialmente tiene implementaciones duplicadas de cada uno de los métodos de la clase que implementan esa interfaz, respaldada por punteros a la la misma implementación física que las entradas vtable propias de la clase.
Aunque podría estar completamente equivocado.
Ese artículo tiene más de 10 años y mucho ha cambiado desde entonces.
Los mapas de IV ahora han sido reemplazados por Virtual Stub Dispatch .
El envío de stub virtual (VSD) es la técnica de usar stubs para invocaciones de métodos virtuales en lugar de la tabla de métodos virtuales tradicionales. En el pasado, el despacho de interfaz requería que las interfaces tuvieran identificadores únicos de proceso, y que cada interfaz cargada se añadiera a un mapa de tabla virtual de interfaz global.
Ve a leer ese artículo, tiene más detalles que nunca necesitarás saber. Viene del Libro del tiempo de ejecución , que fue una documentación escrita originalmente por los desarrolladores de CLR para desarrolladores de CLR pero que ahora se ha publicado para todos. Básicamente describe las agallas del tiempo de ejecución.
No tengo sentido duplicar el artículo aquí, pero simplemente expondré los puntos principales y lo que implican:
- Cuando el JIT ve una llamada a un miembro de la interfaz, la compila en un talón de búsqueda . Esta es una pieza de código que invocará una resolución genérica .
- El resolver genérico es una función que descubrirá qué método llamar. Es la forma más genérica y, por lo tanto, más lenta de invocar tal método. Cuando se le llama por primera vez desde un talón de búsqueda , parcheará ese talón (reescribirá su código en tiempo de ejecución) en un talón de envío . También genera un trozo de resolución para su uso posterior. El código de búsqueda desaparece en este punto.
- Un talón de envío es la forma más rápida de invocar a un miembro de la interfaz, pero hay un problema: es optimista acerca de que la llamada sea monomórfica , lo que significa que está optimizada para el caso en que la llamada de la interfaz siempre se resuelve en el mismo tipo concreto. Compara la tabla de métodos (es decir, el tipo concreto) del objeto con el que se vio anteriormente (que está codificado en el código auxiliar), y llama al método almacenado en caché (cuya dirección también está programada) si la comparación tiene éxito. Si falla, vuelve al apéndice de resolución .
- El resguardo de resolución maneja llamadas polimórficas (el caso general). Utiliza un caché para encontrar qué método llamar. Si el método no está en la memoria caché, invoca la resolución genérica (que también escribe en esta memoria caché).
Y aquí hay una consideración importante, directamente del artículo:
Cuando un talón de envío falla con la frecuencia suficiente, se considera que el sitio de llamada es polimórfico y el talón de resolución volverá a parchear el sitio de llamada para que apunte directamente al talón de resolución para evitar la sobrecarga de un talón de envío que falla constantemente. En los puntos de sincronización (actualmente el final de un GC), los sitios polimórficos se promocionarán aleatoriamente de nuevo a sitios de llamada monomórficos bajo el supuesto de que el atributo polimórfico de un sitio de llamada suele ser temporal. Si esta suposición es incorrecta para cualquier sitio de llamada en particular, activará rápidamente un backpatch para degradarlo a polimórfico nuevamente.
El tiempo de ejecución es realmente optimista acerca de los sitios de llamadas monomórficas, lo que tiene mucho sentido en el código real, y se esforzará por evitar la resolución de los stubs tanto como sea posible.
Si observa el diagrama que estaba en el sitio vinculado, puede facilitar su comprensión.
¿Significa esto que todos los tipos en el mismo proceso tienen el mismo puntero al mismo IVMap?
Sí, ya que está en el nivel de dominio, significa que todo en ese dominio de aplicación tiene el mismo IVMap.
¿Cómo sabe el CLR qué entrada elegir? ¿Hace una búsqueda lineal para encontrar la entrada que coincida con el tipo actual? ¿O una búsqueda binaria? ¿O algún tipo de indexación directa y tener un mapa con posiblemente muchas entradas vacías en él?
Las clases se organizan con compensaciones, por lo que todo tiene un área relativamente establecida en donde estaría. Eso facilita las cosas cuando se buscan métodos. Buscaría en la tabla IVMap y encontraría ese método desde la interfaz. A partir de ahí, va al MethodSlotTable y utiliza la implementación de la interfaz de esa clase. El mapa de la interfaz de la clase contiene los metadatos, sin embargo, la implementación se trata como cualquier otro método.
Nuevamente desde el sitio que vinculó:
Cada implementación de interfaz tendrá una entrada en IVMap. Si MyInterface1 se implementa mediante dos clases, habrá dos entradas en la tabla IVMap. La entrada apuntará al comienzo de la sub-tabla incrustada dentro de la tabla del método MyClass
Esto significa que cada vez que se implementa una interfaz tiene un registro único en el IVMap que apunta a la tabla de tabla de métodos que a su vez apunta a la implementación. Así que sabe qué implementación elegir según la clase que la llama, ya que el registro IVMap apunta al MethodSlotTable en la clase que llama al método. Así que me imagino que es solo una búsqueda lineal a través del IVMap para encontrar la instancia correcta y luego están fuera y funcionando.
EDITAR: Para proporcionar más información sobre el IVMap.
Nuevamente, desde el enlace en el OP:
Los primeros 4 bytes de la primera entrada de InterfaceInfo apuntan a TypeHandle of MyInterface1 (vea la Figura 9 y la Figura 10). El siguiente WORD (2 bytes) es ocupado por Flags (donde 0 se hereda del padre y 1 se implementa en la clase actual). Justo después de Flags es Start Slot, que utiliza el cargador de clases para diseñar la sub-tabla de implementación de la interfaz.
Así que aquí tenemos una tabla donde el número es el desplazamiento de bytes. Este es solo un registro en el IVMap:
+----------------------------------+
| 0 - InterfaceInfo |
+----------------------------------+
| 4 - Parent |
+----------------------------------+
| 5 - Current Class |
+----------------------------------+
| 6 - Start Slot (2 Bytes) |
+----------------------------------+
Supongamos que hay 100 registros de interfaz en este dominio de aplicación y necesitamos encontrar la implementación para cada uno. Simplemente comparamos el 5to byte para ver si coincide con nuestra clase actual y si lo hace, saltamos al código en el 6to byte. Como cada registro tiene 8 bytes de longitud, tendríamos que hacer algo como esto: (Psuedocode)
findclass :
if (!position == class)
findclass adjust offset by 8 and try again
Si bien sigue siendo una búsqueda lineal, en realidad, no va a tomar tanto tiempo ya que el tamaño de los datos que se repiten no es enorme. Espero que eso ayude.
EDIT2:
Entonces, después de mirar el diagrama y preguntarme por qué no hay una Ranura 1 en el IVMap para la clase en el diagrama, releí la sección y encontré esto:
IVMap se crea en base a la información del Mapa de Interfaz incrustada en la tabla de métodos. El Mapa de Interfaz se crea en base a los metadatos de la clase durante el proceso de diseño de la Tabla de Métodos. Una vez que se completa la tipificación, solo se usa IVMap en el envío de métodos.
Por lo tanto, el IVMap para una clase solo se carga con las interfaces que hereda la clase específica. Parece que se copia desde el Mapa de Dominio IV pero que solo mantiene las interfaces a las que se apunta. Esto trae a colación otra pregunta, ¿cómo? Lo más probable es que sea el equivalente de cómo C ++ hace vtables donde cada entrada tiene un desplazamiento y el Mapa de Interfaz proporciona una lista de las compensaciones que se incluirán en el IVMap.
Si observamos el IVMap que podría ser para todo este dominio:
+-------------------------+
| Slot 1 - YourInterface |
+-------------------------+
| Slot 2 - MyInterface |
+-------------------------+
| Slot 3 - MyInterface2 |
+-------------------------+
| Slot 4 - YourInterface2 |
+-------------------------+
Supongamos que solo hay 4 implementaciones de mapa de interfaz en este dominio. Cada ranura tendría un desplazamiento (similar al registro IVMap que publiqué anteriormente) y el IVMap para esta clase usaría esas compensaciones para acceder al registro en el IVMap.
Supongamos que cada ranura tiene 8 bytes y que la ranura 1 comienza en 0, por lo que si quisiéramos obtener la ranura 2 y 3 haríamos algo como esto:
mov ecx,edi
mov eax, dword ptr [ecx]
mov eax, dword ptr [ecx+08h] ; slot 2
; do stuff with slot 2
mov eax, dword ptr [ecx+10h] ; slot 3
; do stuff with slot 3
Disculpe mi x86 ya que no estoy tan familiarizado con él, pero traté de copiar lo que tenían en el artículo al que estaba vinculado.