c# - Comparación redundante y "si" antes de la asignación
.net if-statement (7)
Aquí está el ejemplo:
if(value != ageValue) {
ageValue = value;
}
Quiero decir, si asignamos el valor de una variable a otra, ¿por qué deberíamos comprobar 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;
}
}
}
Aquí hay un ejemplo de código cuando la comprobació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();
...
}
De hecho, he codificado cosas como esta unas cuantas 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 la repugnancia de este escenario, y es desagradable tener que lidiar con él, pero aún así es 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.
- Esto sucede mucho en los programas de matemáticas, por ejemplo, Mathematica, donde no puedes usar números primitivos, lo que te permite terminar con diferentes objetos destinados a representar lo mismo.
-
La referencia de
value
es útil para una lógica de almacenamiento en caché.- Esto también puede aparecer cuando se utilizan valores numéricos abstractos. Por ejemplo, si espera que otras partes del programa tengan datos en caché sobre una referencia, entonces no desea reemplazarlo con una referencia lógicamente equivalente, ya que invalidará los cachés utilizados en otros lugares.
-
Está utilizando un evaluador reactivo, donde establecer un nuevo valor puede forzar una reacción en cadena de actualizaciones.
- Exactamente cómo y por qué esto importa varía dependiendo del contexto.
El gran punto conceptual es que, en algunos casos, puede tener el mismo valor lógico almacenado en diferentes referencias, pero desea intentar minimizar el número 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 referencia como acceso directo, por ejemplo, a través del almacenamiento en caché, que puede ser más eficiente si evita 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 basura puede experimentar ganancias si usted conserva preferencialmente la referencia anterior, ya que se trata de una generación más privilegiada, lo que permite que la referencia más reciente obtenga la basura recolectada antes.
Otro caso de uso, de nuevo con tipos de datos abstractos, es donde podría tener propiedades evaluadas de forma perezosa adjunta a ellos.
Por ejemplo, supongamos que tiene un
abstract class Number
que tiene propiedades como
.IsRational
,
.IsEven
, etc. Entonces, es posible que no las calcule de inmediato, sino que las genere a petición, almacenando en caché los resultados.
En un escenario como este, es posible que prefiera conservar los
Number
más antiguos del mismo valor lógico, ya que pueden tener más cosas adjuntas, mientras que un nuevo
value
puede tener menos información asociada, incluso si es lógico
==
.
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 usarlo. Si no tiene ninguna razón para usarlo, entonces es mejor que no se preocupe por eso hasta que surja alguna motivación.
El
if
es, en la inspección,
no es
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 verificadas podrían implementarse como propiedades, que también pueden tener efectos secundarios en la evaluación.
El rendimiento no es un gran problema, solo depende de sus necesidades lógicas.
En un control de winforms habíamos establecido BackgroundColor a un color específico:
myControl.BackgroundColor = Color.White
En circunstancias específicas, esto podría suceder en un circuito cerrado y conducir a una IU congelada. Después de algunos análisis de rendimiento, encontramos que esta llamada fue la razón de la IU congelada, por lo que simplemente la cambiamos a:
if (myControl.BackgroundColor != Color.White)
myControl.BackgroundColor = Color.White
Y el rendimiento de nuestra herramienta volvió a la normalidad (y luego eliminamos la razón del ciclo cerrado).
Así que esta comprobación no siempre es redundante. Especialmente si el objetivo es una propiedad que hace más dentro del configurador, simplemente aplicando el valor a un almacén de respaldo.
Esta pregunta ha ganado algunos comentarios, pero hasta ahora todas las respuestas intentan replantear la pregunta para abordar los problemas con la sobrecarga del operador o los efectos secundarios del configurador.
Si el setter es usado por múltiples hilos, realmente puede hacer una diferencia. La comprobación antes de establecer el patrón puede (debe medir) ser útil si está iterando sobre los mismos datos con varios subprocesos que alteran los datos. El nombre del libro de texto para este fenómeno se llama compartir falso . Si leyó los datos y verificó que ya coinciden con el valor objetivo, puede omitir la escritura.
Si omite la escritura, la CPU no necesita limpiar la línea de caché (un bloque de 64 bytes en las CPU de Intel) para asegurarse de que otros núcleos vean el valor cambiado. Si el otro núcleo estaba a punto de leer algunos otros datos de ese bloque de 64 bytes, simplemente ha reducido la velocidad de su núcleo y ha aumentado el tráfico entre núcleos para sincronizar los contenidos de la memoria entre las cachés 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 correr eso 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 2
Esto se traduce en un rendimiento un 22% más rápido, lo que puede ser significativo en los escenarios de procesamiento de números de alto rendimiento.
Para contestar la pregunta como estaba escrita:
Puede eliminar la sentencia if si el acceso a la memoria es solo un hilo. Si hay varios subprocesos trabajando en la misma información o en una información cercana, puede ocurrir un intercambio falso que puede costar hasta ca. 20% de rendimiento de acceso a memoria.
Actualización 1 He realizado más pruebas y he creado un gráfico para mostrar el chat de chit de núcleo cruzado. Esto muestra un conjunto simple ( Conjunto incondicional ) como lo anotó el comentarista Frank Hopkins. Condicional no establecido 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.
Sí, esto
if
es inútil.
Comprueba si el valor es el mismo (y configúrelo 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; }
}