Estructura y solo lectura: cómo evitar la degradación del rendimiento

Usar el tipo Struct y el modificador de solo lectura a veces puede causar una degradación del rendimiento. Hoy hablaremos sobre cómo evitar esto usando un analizador de código de código abierto: ErrorProne.NET.



Como probablemente sepa por mis publicaciones anteriores, " El modificador 'in' y las estructuras de solo lectura en C # " ("El modificador en y las estructuras de solo lectura en C #") y " Trampas de rendimiento de locales de referencia y devoluciones de referencia en C # " (" Las trampas de rendimiento cuando se usan variables locales y valores de retorno con el modificador de referencia)), trabajar con estructuras es más difícil de lo que parece. Dejando de lado el tema de la mutabilidad, observo que el comportamiento de las estructuras con modificador de solo lectura (solo lectura) y sin él en contextos de solo lectura varía mucho.

Se supone que las estructuras se utilizan en scripts de programación que requieren un alto rendimiento, y para trabajar de manera efectiva con ellas, debe saber algo sobre las diversas operaciones ocultas generadas por el compilador para garantizar que la estructura permanezca sin cambios.

Aquí hay una breve lista de precauciones que debe recordar:

  • El uso de grandes estructuras que se pasan o devuelven por valor puede causar problemas de rendimiento en las rutas críticas de ejecución del programa.
  • xY hace que se xY una copia protectora de x si:
    • x es un campo de solo lectura;
    • tipo x es una estructura sin modificador de solo lectura;
    • Y no es un campo.

Las mismas reglas funcionan si x es un parámetro con el modificador in, una variable local con el modificador de solo lectura de referencia o el resultado de llamar a un método que devuelve un valor a través de una referencia de solo lectura.

Aquí hay algunas reglas a tener en cuenta. Y, lo más importante, el código que se basa en estas reglas es muy frágil (es decir, los cambios realizados en el código producen inmediatamente cambios significativos en otras partes del código o la documentación, aprox. Transl.). ¿Cuántas personas notarán que reemplazando public readonly int X ; en public int X { get; } public int X { get; } en una estructura de uso frecuente sin un modificador de solo lectura que afecta significativamente el rendimiento? ¿O qué tan fácil es ver que pasar un parámetro usando el modificador in en lugar de pasar por valor puede disminuir el rendimiento? Esto es realmente posible cuando se usa la propiedad in de un parámetro en un bucle, cuando se crea una copia protectora en cada iteración.

Tales propiedades de las estructuras literalmente apelan al desarrollo de analizadores. Y se escuchó la llamada. Conozca ErrorProne.NET : un conjunto de analizadores que le informa sobre la posibilidad de cambiar el código del programa para mejorar su diseño y rendimiento al trabajar con estructuras.

Análisis de código con salida de mensaje "Haga que la estructura X sea de solo lectura"


La mejor manera de evitar errores sutiles e impactos negativos en el rendimiento al usar estructuras es hacer que sean de solo lectura siempre que sea posible. El modificador de solo lectura en la declaración de estructura expresa claramente la intención del desarrollador (enfatizando que la estructura es inmutable) y ayuda al compilador a evitar generar copias de seguridad en muchos de los contextos mencionados anteriormente.



Declarar una estructura de solo lectura no viola la integridad del código. Puede ejecutar de manera segura el fijador (el proceso de arreglar el código) en modo por lotes y declarar todas las estructuras de toda la solución de software de solo lectura.

Amabilidad para el modificador de solo lectura de referencia


El siguiente paso es evaluar la seguridad del uso de nuevas características (en modificador, variables de lectura local, variables de referencia, etc.). Esto significa que el compilador no creará copias protectoras ocultas que puedan reducir el rendimiento.

Se pueden considerar tres tipos de tipos:

  • ref estructuras de solo lectura cuyo uso nunca conduce a la creación de copias protectoras;
  • estructuras que no son amigables para la lectura solo de referencia, cuyo uso en el contexto de solo lectura siempre conduce a la creación de copias protectoras;
  • estructuras neutrales: estructuras cuyo uso puede dar lugar a copias protectoras dependiendo del miembro utilizado en el contexto de solo lectura.

La primera categoría incluye estructuras de solo lectura y estructuras POCO. El compilador nunca generará una copia protectora si la estructura es de solo lectura. También es seguro usar estructuras POCO en el contexto de solo lectura: el acceso a los campos se considera seguro y no se crean copias de protección.

La segunda categoría son estructuras sin modificador de solo lectura que no contienen campos abiertos. En este caso, cualquier acceso al miembro público en el contexto de solo lectura provocará la creación de una copia protectora.

La última categoría es estructuras con campos públicos o internos y propiedades o métodos públicos o internos. En este caso, el compilador crea copias de protección según el miembro utilizado.

Esta separación ayuda a generar advertencias instantáneamente si la estructura "hostil" se pasa con el modificador in, se almacena en la variable local ref readonly, etc.



El analizador no muestra una advertencia si la estructura "hostil" se utiliza como un campo de solo lectura, ya que no hay alternativa en este caso. Los modificadores de solo lectura y referencia están diseñados para ser optimizados específicamente para evitar la creación de copias redundantes. Si la estructura es "hostil" con respecto a estos modificadores, tiene otras opciones: pasar un argumento por valor o guardar una copia en una variable local. En este sentido, los campos de solo lectura se comportan de manera diferente: si desea que el tipo sea inmutable, debe usar estos campos. Recuerde: el código debe ser claro y elegante, y solo secundariamente rápido.

Análisis de CCO


El compilador realiza muchas acciones ocultas para el usuario. Como se muestra en una publicación anterior, es bastante difícil ver cuándo se está creando una copia protectora.

El analizador detecta las siguientes copias ocultas:

  1. Cco del campo de solo lectura.
  2. Bcc de en.
  3. CCO de la variable local de solo lectura de referencia.
  4. Bcc return ref readonly.
  5. Bcc cuando se llama a un método de extensión que toma un parámetro con este modificador por valor para una instancia de la estructura.

 public struct NonReadOnlyStruct { public readonly long PublicField; public long PublicProperty { get; } public void PublicMethod() { } private static readonly NonReadOnlyStruct _ros; public static void Samples(in NonReadOnlyStruct nrs) { // Ok. Public field access causes no hidden copies var x = nrs.PublicField; // Ok. No hidden copies. x = _ros.PublicField; // Hidden copy: Property access on 'in'-parameter x = nrs.PublicProperty; // Hidden copy: Method call on readonly field _ros.PublicMethod(); ref readonly var local = ref nrs; // Hidden copy: method call on ref readonly local local.PublicMethod(); // Hidden copy: method call on ref readonly return Local().PublicMethod(); ref readonly NonReadOnlyStruct Local() => ref _ros; } } 

Tenga en cuenta que los analizadores muestran mensajes de diagnóstico solo si el tamaño de la estructura es ≥16 bytes.

Uso de analizadores en proyectos reales.


La transferencia de grandes estructuras por valor y, como resultado, la creación de copias de protección por parte del compilador afectan significativamente el rendimiento. Al menos esto se muestra en los resultados de las pruebas de rendimiento. Pero, ¿cómo afectarán estos fenómenos a las aplicaciones reales en términos de tiempo de extremo a extremo?

Para probar los analizadores con código real, los utilicé para dos proyectos: el proyecto Roslyn y el proyecto interno en el que estoy trabajando actualmente en Microsoft (el proyecto es una aplicación informática independiente con estrictos requisitos de rendimiento); vamos a llamarlo "Proyecto D" para mayor claridad.

Aquí están los resultados:

  1. Los proyectos con requisitos de alto rendimiento generalmente contienen muchas estructuras, y la mayoría de ellas pueden ser de solo lectura. Por ejemplo, en el proyecto Roslyn, el analizador encontró unas 400 estructuras que pueden ser de solo lectura, y en el proyecto D, unas 300.
  2. En proyectos con requisitos de alto rendimiento, las copias ocultas solo deben crearse en situaciones excepcionales. Encontré solo unos pocos casos en el proyecto Roslyn, ya que la mayoría de las estructuras tienen campos públicos en lugar de propiedades públicas. Esto evita la creación de copias de protección en situaciones donde las estructuras se almacenan en campos de solo lectura. Hubo más copias ocultas en el Proyecto D, porque al menos la mitad de ellas tenían propiedades de solo obtención (acceso de solo lectura).
  3. Es probable que la transferencia de estructuras incluso bastante grandes que usan el modificador in tenga un efecto muy pequeño (casi imperceptible) en el tiempo completo del programa.

Cambié las 300 estructuras en el proyecto D, haciéndolas de solo lectura, y luego corregí cientos de casos de su uso, lo que indica que se pasan con el modificador in. Luego medí el tiempo de tránsito de extremo a extremo para varios escenarios de rendimiento. Las diferencias fueron estadísticamente insignificantes.

¿Significa esto que las características descritas anteriormente son inútiles? En absoluto

Trabajar en un proyecto con requisitos de alto rendimiento (por ejemplo, en Roslyn o "Proyecto D") implica que una gran cantidad de personas dedican mucho tiempo a varios tipos de optimización. De hecho, en algunos casos, las estructuras en nuestro código se pasaron con el modificador de referencia, y algunos campos se declararon sin el modificador de solo lectura para excluir la generación de copias protectoras. La falta de crecimiento de la productividad durante la transferencia de estructuras con el modificador in puede significar que el código estaba bien optimizado y que no hay copia excesiva de estructuras en los caminos críticos de su paso.

¿Qué debo hacer con estas características?


Creo que la cuestión del uso del modificador de solo lectura para estructuras no requiere mucha reflexión. Si la estructura es inmutable, entonces el modificador de solo lectura simplemente obliga explícitamente al compilador a tal decisión de diseño. Y la falta de copias protectoras para tales estructuras es solo una ventaja.

Hoy mis recomendaciones son las siguientes: si la estructura puede ser de solo lectura, entonces háganlo así.

Usar las otras opciones consideradas tiene matices.

¿Optimización previa versus pesimización previa?


Herb Sutter presenta el concepto de "pesimismo preliminar" en su sorprendente libro, C ++ Coding Standards: 101 Rule, Recommended, and Best Practices .

“Ceteris paribus, complejidad de código y legibilidad, algunos patrones de diseño efectivos y expresiones idiomáticas de codificación naturalmente deberían drenarse de la punta de tus dedos. Tal código no es más difícil de escribir que sus alternativas pesimistas. No se realiza una optimización preliminar, sino que se evita la pesimismo voluntaria ".

Desde mi punto de vista, un parámetro con el modificador in es el caso. Si sabe que la estructura es relativamente grande (40 bytes o más), siempre puede pasarla con el modificador in. El costo de usar el modificador in es relativamente bajo, ya que no es necesario ajustar las llamadas, y los beneficios pueden ser reales.

Por el contrario, para variables locales y valores de retorno con el modificador de referencia de solo lectura, este no es el caso. Diría que estas características deberían usarse al codificar bibliotecas, y es mejor rechazarlas en el código de la aplicación (solo si la creación de perfiles del código no revela que la operación de copia es realmente un problema). El uso de estas características requiere un esfuerzo adicional, y se vuelve más difícil para el lector de códigos entenderlo.

Conclusión


  1. Utilice el modificador de solo lectura para estructuras donde sea posible.
  2. Considere usar el modificador in para estructuras grandes.
  3. Considere el uso de variables locales y valores de retorno con el modificador de solo lectura de referencia para codificar bibliotecas o en los casos en que los resultados del perfil de código indiquen que esto puede ser útil.
  4. Use ErrorProne.NET para detectar problemas de código y compartir los resultados.

Source: https://habr.com/ru/post/es423053/


All Articles