Pregunta

Antes de reaccionar desde el instinto, como lo hice inicialmente, lea la pregunta completa, por favor. Sé que te hacen sentir sucio, sé que todos hemos sido quemados antes y sé que no es "buen estilo" pero, ¿los campos públicos siempre están bien?

Estoy trabajando en una aplicación de ingeniería a gran escala que crea y trabaja con un modelo en memoria de una estructura (desde el edificio de gran altura hasta el puente, el cobertizo, no importa). Hay una tonelada de análisis y cálculos geométricos involucrados en este proyecto. Para apoyar esto, el modelo está compuesto por muchas estructuras pequeñas de solo lectura inmutables para representar cosas como puntos, segmentos de línea, etc. Se accede a algunos de los valores de estas estructuras (como las coordenadas de los puntos) decenas o cientos de millones de Tiempos durante la ejecución de un programa típico. Debido a la complejidad de los modelos y el volumen de cálculo, el rendimiento es absolutamente crítico.

Siento que estamos haciendo todo lo posible para optimizar nuestros algoritmos, pruebas de rendimiento para determinar cuellos de botella, usar las estructuras de datos correctas, etc. No creo que este sea un caso de optimización prematura. Las pruebas de rendimiento muestran el orden de magnitud (al menos) las mejoras de rendimiento cuando se accede a los campos directamente en lugar de a través de una propiedad en el objeto. Dada esta información, y el hecho de que también podemos exponer la misma información que las propiedades para respaldar el enlace de datos y otras situaciones ... ¿está bien? Recuerda, lee solo los campos en estructuras inmutables. ¿Alguien puede pensar en una razón por la que voy a lamentarme?

Aquí hay una aplicación de prueba de muestra:


struct Point {
    public Point(double x, double y, double z) {
        _x = x;
        _y = y;
        _z = z;
    }

    public readonly double _x;
    public readonly double _y;
    public readonly double _z;

    public double X { get { return _x; } }
    public double Y { get { return _y; } }
    public double Z { get { return _z; } }
}

class Program {
    static void Main(string[] args) {
        const int loopCount = 10000000;

        var point = new Point(12.0, 123.5, 0.123);

        var sw = new Stopwatch();
        double x, y, z;
        double calculatedValue;
        sw.Start();
        for (int i = 0; i < loopCount; i++) {
            x = point._x;
            y = point._y;
            z = point._z;
            calculatedValue = point._x * point._y / point._z;
        }
        sw.Stop();
        double fieldTime = sw.ElapsedMilliseconds;
        Console.WriteLine("Direct field access: " + fieldTime);

        sw.Reset();
        sw.Start();
        for (int i = 0; i < loopCount; i++) {
            x = point.X;
            y = point.Y;
            z = point.Z;
            calculatedValue = point.X * point.Y / point.Z;
        }
        sw.Stop();
        double propertyTime = sw.ElapsedMilliseconds;
        Console.WriteLine("Property access: " + propertyTime);

        double totalDiff = propertyTime - fieldTime;
        Console.WriteLine("Total difference: " + totalDiff);
        double averageDiff = totalDiff / loopCount;
        Console.WriteLine("Average difference: " + averageDiff);

        Console.ReadLine();
    }
}

resultado:
Acceso directo al campo: 3262
Acceso a la propiedad: 24248
Diferencia total: 20986
Diferencia media: 0.00020986


Es solo 21 segundos, pero ¿por qué no?

¿Fue útil?

Solución

Su prueba no es realmente justa para las versiones basadas en la propiedad. El JIT es lo suficientemente inteligente como para incluir propiedades simples en línea para que tengan un rendimiento de tiempo de ejecución equivalente al del acceso directo al campo, pero no parece lo suficientemente inteligente (hoy) para detectar cuándo las propiedades acceden a valores constantes.

En su ejemplo, todo el cuerpo del bucle de la versión de acceso al campo se optimiza, convirtiéndose simplemente en:

for (int i = 0; i < loopCount; i++)
00000025  xor         eax,eax 
00000027  inc         eax  
00000028  cmp         eax,989680h 
0000002d  jl          00000027 
}

mientras que la segunda versión, en realidad está realizando la división de punto flotante en cada iteración:

for (int i = 0; i < loopCount; i++)
00000094  xor         eax,eax 
00000096  fld         dword ptr ds:[01300210h] 
0000009c  fdiv        qword ptr ds:[01300218h] 
000000a2  fstp        st(0) 
000000a4  inc         eax  
000000a5  cmp         eax,989680h 
000000aa  jl          00000096 
}

Hacer solo dos pequeños cambios en tu aplicación para hacerla más realista hace que las dos operaciones sean prácticamente idénticas en rendimiento.

Primero, aleatoriza los valores de entrada para que no sean constantes y el JIT no sea lo suficientemente inteligente como para eliminar la división por completo.

Cambiar de:

Point point = new Point(12.0, 123.5, 0.123);

a:

Random r = new Random();
Point point = new Point(r.NextDouble(), r.NextDouble(), r.NextDouble());

En segundo lugar, asegúrese de que los resultados de cada iteración de bucle se utilicen en alguna parte:

Antes de cada bucle, establece el valor calculado = 0 para que ambos comiencen en el mismo punto. Después de cada bucle, llame a Console.WriteLine (calculadoValue.ToString ()) para asegurarse de que el resultado se usa " " Así que el compilador no lo optimiza. Por último, cambie el cuerpo del bucle de " calculadoValor = ... " a " valor calculado + = ... " para que se use cada iteración.

En mi máquina, estos cambios (con una versión de lanzamiento) producen los siguientes resultados:

Direct field access: 133
Property access: 133
Total difference: 0
Average difference: 0

Tal como esperamos, el x86 para cada uno de estos bucles modificados es idéntico (excepto por la dirección del bucle)

000000dd  xor         eax,eax 
000000df  fld         qword ptr [esp+20h] 
000000e3  fmul        qword ptr [esp+28h] 
000000e7  fdiv        qword ptr [esp+30h] 
000000eb  fstp        st(0) 
000000ed  inc         eax  
000000ee  cmp         eax,989680h 
000000f3  jl          000000DF (This loop address is the only difference) 

Otros consejos

Dado que usted trata con objetos inmutables con campos de solo lectura, yo diría que ha afectado al único caso cuando no encuentro que los campos públicos sean un hábito sucio.

OMI, el " no hay campos públicos " La regla es una de esas reglas que son técnicamente correctas, pero a menos que esté diseñando una biblioteca para el público, es poco probable que le cause algún problema si la rompe.

Antes de obtener una votación demasiado masiva, debo agregar que encapsulación es algo bueno. Dada la invariable " la propiedad Value debe ser nula si HasValue es false " ;, este diseño es defectuoso:

class A {
    public bool HasValue;
    public object Value;
}

Sin embargo, dado ese invariante, este diseño es igualmente defectuoso:

class A {
    public bool HasValue { get; set; }
    public object Value { get; set; }
}

El diseño correcto es

class A {
    public bool HasValue { get; private set; }
    public object Value { get; private set; }

    public void SetValue(bool hasValue, object value) {
        if (!hasValue && value != null)
            throw new ArgumentException();
        this.HasValue = hasValue;
        this.Value    = value;
    }
}

(e incluso mejor sería proporcionar un constructor de inicialización y hacer que la clase sea inmutable).

Sé que te sientes un poco sucio al hacer esto, pero no es raro que las reglas y las pautas se vuele al infierno cuando el rendimiento se convierte en un problema. Por ejemplo, algunos sitios web de alto tráfico que usan MySQL tienen duplicación de datos y tablas desnormalizadas. Otros incluso se vuelven más locos .

Moraleja de la historia: puede ir en contra de todo lo que le enseñaron o aconsejaron, pero los puntos de referencia no mienten. Si funciona mejor, simplemente hazlo.

Si realmente necesita ese rendimiento adicional, entonces probablemente es lo correcto. Si no necesita el rendimiento adicional, entonces probablemente no lo sea.

Rico Mariani tiene un par de publicaciones relacionadas:

Personalmente, la única vez que consideraría utilizar campos públicos es en una clase anidada privada muy específica de la implementación.

Otras veces simplemente se siente mal " mal " para hacerlo.

El CLR se encargará del rendimiento al optimizar el método / propiedad (en versiones de lanzamiento) para que no sea un problema.

No es que no esté de acuerdo con las otras respuestas, o con tu conclusión ... pero me gustaría saber de dónde obtienes el estado de diferencia de rendimiento de orden de magnitud. Como entiendo el compilador de C #, cualquier propiedad simple (sin otro código adicional que no sea el acceso directo al campo), debe ser incorporada por el compilador JIT como un acceso directo de todos modos.

La ventaja de usar propiedades incluso en estos casos simples (en la mayoría de las situaciones) era que al escribirlas como una propiedad, se permiten cambios futuros que podrían modificar la propiedad. (Aunque, en su caso, no habrá cambios de este tipo en el futuro, por supuesto)

Intente compilar una versión de compilación y ejecutarla directamente desde el exe en lugar de hacerlo a través del depurador. Si la aplicación se ejecutó a través de un depurador, el compilador JIT no alineará los accesores de propiedades. No pude replicar sus resultados. De hecho, cada prueba que realicé indicó que prácticamente no hubo diferencias en el tiempo de ejecución.

Pero, como los demás, no estoy completamente opuesto al acceso directo al campo. Especialmente porque es fácil hacer que el campo sea privado y agregar un acceso a una propiedad pública más adelante sin necesidad de realizar más modificaciones de código para que la aplicación se compile.

Editar: Bueno, mis pruebas iniciales utilizaron un tipo de datos int en lugar de doble. Veo una gran diferencia cuando uso dobles. Con ints el directo frente a la propiedad es virtualmente el mismo. Con dobles, el acceso a la propiedad es aproximadamente 7x más lento que el acceso directo en mi máquina. Esto es un tanto desconcertante para mí.

Además, es importante ejecutar las pruebas fuera del depurador. Incluso en la versión de compilación, el depurador agrega una sobrecarga que sesga los resultados.

Aquí hay algunos escenarios en los que está bien (del libro de pautas de diseño del marco):

  
      
  • DEBE usar campos constantes para constantes   eso nunca va a cambiar
  •   
  • usar público   campos de solo lectura estáticos para predefinidos   instancias de objeto.
  •   

Y donde no está:

  
      
  • NO asigne instancias de mutables   tipos de campos de solo lectura.
  •   

De lo que ha dicho, no entiendo por qué sus propiedades triviales no se incluyen en el JIT.

Si modifica su prueba para usar las variables temporales que asigna en lugar de acceder directamente a las propiedades en su cálculo, verá una gran mejora en el rendimiento:

        sw.Start();
        for (int i = 0; i < loopCount; i++)
        {
            x = point._x;
            y = point._y;
            z = point._z;
            calculatedValue = x * y / z;
        }
        sw.Stop();
        double fieldTime = sw.ElapsedMilliseconds;
        Console.WriteLine("Direct field access: " + fieldTime);

        sw.Reset();
        sw.Start();
        for (int i = 0; i < loopCount; i++)
        {
            x = point.X;
            y = point.Y;
            z = point.Z;
            calculatedValue = x * y / z;
        }
        sw.Stop();

Tal vez repita a alguien más, pero este es mi punto también si puede ayudar.

Las enseñanzas son para darte las herramientas que necesitas para lograr un cierto nivel de facilidad cuando te encuentres en tales situaciones.

La metodología de desarrollo de Agile Software dice que primero debe entregar el producto a su cliente sin importar cómo se vea su código. En segundo lugar, puede optimizar y hacer que su código " hermoso " o según los estados de programación del art.

Aquí, usted o su cliente requieren rendimiento. Dentro de su proyecto, el DESEMPEÑO es CRUCIAL, si lo entiendo correctamente.

Entonces, supongo que estarás de acuerdo conmigo en que no nos importa cómo se ve el código o si respeta el " art " ;. ¡Haz lo que tienes que hacer para que sea potente y potente! Las propiedades le permiten a su código formatear " " los datos de E / S si es necesario. Una propiedad tiene su propia dirección de memoria, luego busca su dirección de miembro cuando devuelve el valor del miembro, por lo que obtuvo dos búsquedas de dirección. Si el rendimiento es tan crítico, solo hazlo y haz públicos a tus miembros inmutables. :-)

Esto también refleja otros puntos de vista, si leo correctamente. :)

¡Que tengas un buen día!

Los tipos que encapsulan la funcionalidad deben usar propiedades. Los tipos que solo sirven para guardar datos deben usar campos públicos, excepto en el caso de clases inmutables (donde los campos de ajuste en propiedades de solo lectura son la única forma de protegerlos de manera confiable contra modificaciones). Exponer a los miembros como campos públicos esencialmente proclama "estos miembros pueden modificarse libremente en cualquier momento sin tener en cuenta nada más". Si el tipo en cuestión es un tipo de clase, además proclama que "cualquiera que exponga una referencia a esta cosa permitirá que el destinatario cambie estos miembros en cualquier momento y en la forma que crea conveniente". Si bien no se deberían exponer los campos públicos en los casos en que dicha proclamación sería inapropiada, se deberían exponer los campos públicos en los casos en que dicha proclamación sería apropiada y el código del cliente podría beneficiarse de las suposiciones habilitadas de ese modo.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top