Резервное сравнение и "если" перед назначением

Вот пример:

if(value != ageValue) {
  ageValue = value;
}

Я имею в виду, если мы присваиваем значение переменной другому, зачем нам проверять, имеют ли они в любом случае одинаковое значение?

Это смущает меня. Вот более широкий контекст:

private double ageValue;
public double Age {
  get {
    return ageValue;
  }

  set {
    if(value != ageValue) {
      ageValue = value;
    }
  }
}

Ответ 1

Вот пример кода, когда проверка весьма полезна:

 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(); 
        ...
      } 
    } 

 ... 

Однако более естественной реализацией является проверка в самом начале, чтобы избежать ненужных вычислений.

    protected set {
      if (ageValue == value)
        return;

      ... // value validation here
      ageValue = value; 

      // ageValue has been changed
      // Time (or / and memory) consuming process
      SaveToRDBMS();
      InvalidateCache();  
      ...
    }

Ответ 2

В элементе управления winforms мы установили BackgroundColor для определенного цвета:

myControl.BackgroundColor = Color.White

При определенных обстоятельствах это может произойти в узком цикле и привести к зависанию интерфейса. После некоторого анализа производительности мы обнаружили, что этот вызов был причиной зависания интерфейса, поэтому мы просто изменили его на:

if (myControl.BackgroundColor != Color.White)
    myControl.BackgroundColor = Color.White

И производительность нашего инструмента вернулась в норму (а затем мы устранили причину жесткой циклы).

Так что эта проверка не всегда избыточна. Особенно, если цель - это свойство, которое больше действует в установщике, чем просто применяет значение к резервному хранилищу.

Ответ 3

if, при осмотре, не является лишним. Это зависит от оставшейся реализации. Обратите внимание, что в С# != Может быть перегружен, что означает, что оценка может иметь побочные эффекты. Кроме того, проверенные переменные могут быть реализованы в виде свойств, которые также могут оказывать побочные эффекты на оценку.

Ответ 4

Этот вопрос получил довольно много комментариев, но до сих пор все ответы пытаются перефразировать вопрос для решения проблем с перегрузкой оператора или побочными эффектами установщика.

Если сеттер используется несколькими потоками, это действительно может изменить ситуацию. Проверка перед заданным шаблоном может (вам следует измерить) быть полезной, если вы перебираете одни и те же данные с несколькими потоками, которые изменяют данные. Название учебника для этого явления называется ложным делением. Если вы прочитали данные и убедились, что они уже соответствуют целевому значению, вы можете пропустить запись.

Если вы пропустите запись, CPU не нужно очищать строку кэша (64-байтовый блок на процессорах Intel), чтобы другие ядра увидели измененное значение. Если другое ядро собиралось прочитать некоторые другие данные из этого 64-байтового блока, то вы просто замедлили свое ядро и увеличили многоядерный трафик, чтобы синхронизировать содержимое памяти между кэшами ЦП.

В следующем примере приложения показан этот эффект, который также содержит проверку перед записью:

 if (tmp1 != checkValue)  // set only if not equal to checkvalue
 {
    values[i] = checkValue;
 }

Вот полный код:

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}");
    }
}

Если вы позволите этому запустить, вы получите такие значения, как:

// 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

Это приводит к увеличению производительности на 22%, что может быть значительным в сценариях с высокой производительностью.

Чтобы ответить на вопрос как было написано:

Вы можете удалить оператор if, если доступ к памяти только однопоточный. Если несколько потоков работают с одинаковыми или близкими данными, может произойти ложное совместное использование, которое может стоить до приблизительно. 20% производительности доступа к памяти.

Обновление 1 Я провел больше тестов и создал диаграмму, показывающую чат с несколькими ядрами. Это показывает простой набор (безусловный набор), как это было отмечено комментатором Фрэнком Хопкинсом. Условный не указан содержит if, который никогда не устанавливает значение. И последнее, но не менее Условный Set будет установить значение в, если условиях.

Performance vs Cores

Ответ 5

Я на самом деле закодировал такие вещи несколько раз по разным причинам. Их сложно объяснить, так что терпите меня.

Главное, чтобы вы не устанавливали новую ссылку, если значение в ссылке логически равно предыдущему значению ссылки. В приведенных выше комментариях пользователи критиковали неприятность этого сценария - и с ним приходится сталкиваться с неприятностями - но все же по существу это необходимо в случаях.

Я бы попытался разделить варианты использования, как это:

  1. Значение является абстрактным типом данных, где у вас могут быть разные составные экземпляры, представляющие одно и то же логическое значение.

    • Это часто случается в математических программах, например, Mathematica, где вы не можете использовать примитивные числа, что позволяет вам получить разные объекты, которые должны представлять одно и то же.
  2. Ссылка на value полезна для логики кэширования.

    • Это также может появиться при использовании абстрактных чисел. Например, если вы ожидаете, что другие части программы будут кэшировать данные о ссылке, вы не захотите заменять ее логически эквивалентной ссылкой, так как это приведет к аннулированию кэшей, используемых в других местах.
  3. Вы используете реактивный оценщик, где установка нового значения может вызвать цепную реакцию обновлений.

    • Как и почему это важно, зависит от контекста.

Большой концептуальный момент заключается в том, что в некоторых случаях вы можете хранить одно и то же логическое значение в разных ссылках, но вы хотите попытаться свести к минимуму количество вырожденных ссылок по двум важным причинам:

  1. Хранение одного и того же логического значения несколько раз приводит к увеличению объема памяти.

  2. Большая часть времени выполнения может использовать проверку ссылок в качестве ярлыка, например, посредством кэширования, что может быть более эффективным, если вы избегаете распространения избыточных ссылок на одно и то же логическое значение.

В другом случайном примере сборщик мусора в .NET является "генерационным", что означает, что он прикладывает больше усилий для проверки возможности сбора значения, когда оно более новое. Таким образом, сборщик мусора может получить выгоду, если вы предпочтительно сохраните более старую ссылку, как это происходит в более привилегированном поколении, что позволяет более новой ссылке быстрее собирать мусор.

Другой вариант использования, опять же с абстрактными типами данных, - это где лениво-оцененные свойства могут быть присоединены к ним. Например, допустим, у вас есть abstract class Number с такими свойствами, как .IsRational, .IsEven и т.д. Затем вы можете не вычислять их сразу, а генерировать их по требованию, кэшируя результаты. В подобном сценарии вы можете предпочесть оставить старое Number с тем же логическим значением, поскольку к ним может быть прикреплено больше материала, тогда как с новым value может быть связано меньше информации, даже если оно логически ==.

Трудно придумать, как суммировать различные причины, по которым это может иметь смысл в некоторых случаях, но в основном это оптимизация, которая может иметь смысл, если у вас есть причина использовать ее. Если у вас нет причин использовать его, то, вероятно, лучше не беспокоиться об этом, пока не возникнет какая-то мотивация.

Ответ 6

Производительность не имеет большого значения, зависит только от ваших логических потребностей.

Ответ 7

Да, это if бесполезно. Вы проверяете, являются ли значения одинаковыми (и устанавливаете его, если нет).

Когда != -operator не перегружен, тогда это:

private double ageValue; 

public double Age 
{ 
    get { return ageValue; } 

    set
    { 
        if (value != ageValue) 
        { 
            ageValue = value; 
        } 
    }
} 

то же самое

private double ageValue; 

public double Age 
{ 
    get { return ageValue; } 
    set { ageValue = value; }
}