Aquí está el ejemplo:
if(value != ageValue) { ageValue = value; }Es decir, si asignamos el valor de una variable a otra, ¿por qué tendríamos que verificar si tienen el mismo valor de todos modos?
Eso me confunde. Aquí está el contexto más amplio:
private double ageValue; public double Age { get { return ageValue; } set { if(value != ageValue) { ageValue = value; } } }Sí, esto if es inútil. Comprueba si el valor es el mismo (y configúralo si no).
Cuando el operador != no está sobrecargado, entonces es esto:
private double ageValue; public double Age { get { return ageValue; } set { if (value != ageValue) { ageValue = value; } } }Lo mismo
private double ageValue; public double Age { get { return ageValue; } set { ageValue = value; } }El if es, en la inspección, no redundante. Depende de la implementación restante. Tenga en cuenta que en C#, != se puede sobrecargar, lo que significa que la evaluación puede tener efectos secundarios. Además, las variables marcadas podrían implementarse como propiedades, lo que también puede tener efectos secundarios en la evaluación.
En un control de winforms habíamos establecido el Color de fondo en un color específico:
myControl.BackgroundColor = Color.WhiteEn circunstancias específicas, esto podría ocurrir en un bucle cerrado y conducir a una interfaz de usuario congelada. Después de un análisis de rendimiento, descubrimos que esta llamada era el motivo de la IU congelada, por lo que simplemente la cambiamos a:
if (myControl.BackgroundColor != Color.White) myControl.BackgroundColor = Color.WhiteY el rendimiento de nuestra herramienta volvió a la normalidad (y luego eliminamos la razón del ciclo cerrado).
Así que esta verificación no siempre es redundante. Especialmente si el objetivo es una propiedad que hace más dentro del setter que simplemente aplicar el valor a una tienda de respaldo.
Aquí hay una muestra de código cuando la verificación es bastante útil :
public class MyClass { ... int ageValue = 0; public int AgeValue { get { return ageValue } protected set { ... // value validation here // your code starts if (value != ageValue) { ageValue = value; } // your code ends else return; // do nothing since value == ageValue // ageValue has been changed // Time (or / and memory) consuming process SaveToRDBMS(); InvalidateCache(); ... } } ...Sin embargo, una implementación más natural es verificar desde el principio para evitar cálculos innecesarios.
protected set { if (ageValue == value) return; ... // value validation here ageValue = value; // ageValue has been changed // Time (or / and memory) consuming process SaveToRDBMS(); InvalidateCache(); ... }Esta pregunta ha ganado bastantes comentarios, pero hasta ahora todas las respuestas intentan reformular la pregunta para abordar problemas con la sobrecarga del operador o los efectos secundarios del colocador.
Si el setter es utilizado por varios subprocesos, realmente puede marcar la diferencia. El patrón de verificación antes de establecer puede (debe medir) ser útil si está iterando sobre los mismos datos con múltiples hilos que alteran los datos. El nombre de libro de texto para este fenómeno se llama intercambio falso . Si leyó los datos y verificó que ya coincide con el valor objetivo, puede omitir la escritura.
Si omite la escritura, la CPU no necesita vaciar la línea de caché (un bloque de 64 bytes en las CPU Intel) para asegurarse de que otros núcleos vean el valor modificado. Si el otro núcleo estaba a punto de leer otros datos de ese bloque de 64 bytes, simplemente ha ralentizado su núcleo y ha aumentado el tráfico entre núcleos para sincronizar el contenido de la memoria entre las memorias caché de la CPU.
La siguiente aplicación de ejemplo muestra este efecto que también contiene la condición de verificación antes de escribir:
if (tmp1 != checkValue) // set only if not equal to checkvalue { values[i] = checkValue; }Aquí está el código completo:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; class Program { static void Main(string[] args) { const int N = 500_000_000; int[] values = new int[N]; // 2 GB for (int nThreads = 1; nThreads < Environment.ProcessorCount; nThreads++) { SetArray(values, checkValue: 1, nTimes: 10, nThreads: nThreads); SetArray(values, checkValue: 2, nTimes: 10, nThreads: nThreads); SetArrayNoCheck(values, checkValue: 2, nTimes: 10, nThreads: nThreads); } } private static void SetArray(int[] values, int checkValue, int nTimes, int nThreads) { List<double> ms = new List<double>(); for (int k = 0; k < nTimes; k++) // set array values to 1 { for (int i = 0; i < values.Length; i++) { values[i] = 1; } var sw = Stopwatch.StartNew(); Action acc = () => { int tmp1 = 0; for (int i = 0; i < values.Length; i++) { tmp1 = values[i]; if (tmp1 != checkValue) // set only if not equal to checkvalue { values[i] = checkValue; } } }; Parallel.Invoke(Enumerable.Repeat(acc, nThreads).ToArray()); // Let this run on 3 cores sw.Stop(); ms.Add(sw.Elapsed.TotalMilliseconds); // Console.WriteLine($"Set {values.Length * 4 / (1_000_000_000.0f):F1} GB of Memory in {sw.Elapsed.TotalMilliseconds:F0} ms. Initial Value 1. Set Value {checkValue}"); } string descr = checkValue == 1 ? "Conditional Not Set" : "Conditional Set"; Console.WriteLine($"{descr}, {ms.Average():F0}, ms, nThreads, {nThreads}"); } private static void SetArrayNoCheck(int[] values, int checkValue, int nTimes, int nThreads) { List<double> ms = new List<double>(); for (int k = 0; k < nTimes; k++) // set array values to 1 { for (int i = 0; i < values.Length; i++) { values[i] = 1; } var sw = Stopwatch.StartNew(); Action acc = () => { for (int i = 0; i < values.Length; i++) { values[i] = checkValue; } }; Parallel.Invoke(Enumerable.Repeat(acc, nThreads).ToArray()); // Let this run on 3 cores sw.Stop(); ms.Add(sw.Elapsed.TotalMilliseconds); //Console.WriteLine($"Unconditional Set {values.Length * 4 / (1_000_000_000.0f):F1} GB of Memory in {sw.Elapsed.TotalMilliseconds:F0} ms. Initial Value 1. Set Value {checkValue}"); } Console.WriteLine($"Unconditional Set, {ms.Average():F0}, ms, nThreads, {nThreads}"); } }Si dejas que se ejecute, obtienes valores como:
// Value not set Set 2.0 GB of Memory in 439 ms. Initial Value 1. Set Value 1 Set 2.0 GB of Memory in 420 ms. Initial Value 1. Set Value 1 Set 2.0 GB of Memory in 429 ms. Initial Value 1. Set Value 1 Set 2.0 GB of Memory in 393 ms. Initial Value 1. Set Value 1 Set 2.0 GB of Memory in 404 ms. Initial Value 1. Set Value 1 Set 2.0 GB of Memory in 395 ms. Initial Value 1. Set Value 1 Set 2.0 GB of Memory in 419 ms. Initial Value 1. Set Value 1 Set 2.0 GB of Memory in 421 ms. Initial Value 1. Set Value 1 Set 2.0 GB of Memory in 442 ms. Initial Value 1. Set Value 1 Set 2.0 GB of Memory in 422 ms. Initial Value 1. Set Value 1 // Value written Set 2.0 GB of Memory in 519 ms. Initial Value 1. Set Value 2 Set 2.0 GB of Memory in 582 ms. Initial Value 1. Set Value 2 Set 2.0 GB of Memory in 543 ms. Initial Value 1. Set Value 2 Set 2.0 GB of Memory in 484 ms. Initial Value 1. Set Value 2 Set 2.0 GB of Memory in 523 ms. Initial Value 1. Set Value 2 Set 2.0 GB of Memory in 540 ms. Initial Value 1. Set Value 2 Set 2.0 GB of Memory in 552 ms. Initial Value 1. Set Value 2 Set 2.0 GB of Memory in 527 ms. Initial Value 1. Set Value 2 Set 2.0 GB of Memory in 535 ms. Initial Value 1. Set Value 2 Set 2.0 GB of Memory in 581 ms. Initial Value 1. Set Value 2Eso da como resultado un rendimiento un 22% más rápido que puede ser significativo en escenarios de procesamiento de números de alto rendimiento.
Para responder a la pregunta como estaba escrito:
Puede eliminar la declaración if si el acceso a la memoria es solo de un solo subproceso. Si varios subprocesos están trabajando en los mismos datos o en datos cercanos, puede ocurrir un intercambio falso que puede costarle hasta aprox. 20% del rendimiento de acceso a la memoria.
Actualización 1 Realicé más pruebas y creé un gráfico para mostrar el chat de chit de núcleo cruzado. Esto muestra un conjunto simple (conjunto incondicional ) como lo señaló el comentarista Frank Hopkins. Conditional Not Set contiene el if que nunca establece el valor. Y por último, pero no menos importante, el conjunto condicional establecerá el valor en la condición if.
De hecho, he codificado cosas como esta varias veces, por diferentes razones. Son un poco difíciles de explicar, así que tengan paciencia conmigo.
Lo principal es que no establece una nueva referencia si el valor en la referencia es lógicamente igual al valor de la referencia anterior. En los comentarios anteriores, los usuarios han criticado lo detestable de este escenario, y es detestable tener que lidiar con él, pero sigue siendo esencialmente necesario en los casos.
Intentaría dividir casos de uso como este:
El valor es un tipo de datos abstracto, donde puede tener diferentes instancias construidas que representan el mismo valor lógico.
La referencia de value es útil para una lógica de almacenamiento en caché.
Está utilizando un evaluador reactivo, donde establecer un nuevo valor puede forzar una reacción en cadena de actualizaciones.
El gran punto conceptual es que, en algunos casos, puede tener el mismo valor lógico almacenado en diferentes referencias, pero desea intentar minimizar la cantidad de referencias degeneradas por dos grandes razones:
Tener el mismo valor lógico almacenado varias veces consume más memoria.
Gran parte del tiempo de ejecución puede usar la verificación de referencias como un atajo, por ejemplo, a través del almacenamiento en caché, que puede ser más eficiente si evita permitir que se propaguen referencias redundantes al mismo valor lógico.
Para otro ejemplo aleatorio, el recolector de basura de .NET es " generacional " , lo que significa que se esfuerza más en verificar si se puede recopilar un valor cuando es más nuevo. Por lo tanto, el recolector de elementos no utilizados puede experimentar ganancias si prefiere conservar la referencia anterior, ya que se encuentra en una generación más privilegiada, lo que permite que la referencia más nueva recolecte elementos no utilizados antes.
Otro caso de uso, de nuevo con tipos de datos abstractos, es donde puede tener propiedades evaluadas de forma perezosa adjuntas a ellos. Por ejemplo, supongamos que tiene un abstract class Number que tiene propiedades como .IsRational , .IsEven , etc. Entonces, es posible que no los calcule de inmediato, sino que los genere a pedido, almacenando en caché los resultados. En un escenario como este, es posible que prefiera conservar los Number anteriores del mismo valor lógico, ya que pueden tener más cosas adjuntas, mientras que un value nuevo puede tener menos información asociada, incluso si es lógicamente == .
Es un poco difícil pensar en cómo resumir las diversas razones por las que esto puede tener sentido en algunos casos, pero básicamente es una optimización que puede tener sentido si tiene una razón para usarla. Si no tiene ninguna razón para usarlo, probablemente sea mejor no preocuparse hasta que surja alguna motivación.
El rendimiento no es un gran problema, solo depende de sus necesidades lógicas.