Избыточное сравнение и «если» перед назначением - PullRequest
52 голосов
/ 22 марта 2019

Вот пример:

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

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

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

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

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

Ответы [ 7 ]

50 голосов
/ 22 марта 2019

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

 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();  
      ...
    }
39 голосов
/ 22 марта 2019

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

myControl.BackgroundColor = Color.White

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

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

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

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

20 голосов
/ 22 марта 2019

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

18 голосов
/ 22 марта 2019

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

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

Если вы пропустите запись, ЦПУ не нужно очищать строку кэша (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, которое никогда не устанавливает значение.И последнее, но не менее важное: Условный набор установит значение в условии if.

Performance vs Cores

1 голос
/ 23 марта 2019

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

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

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

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

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

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

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

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

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

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

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

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

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

0 голосов
/ 11 апреля 2019

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

0 голосов
/ 22 марта 2019

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

Если оператор != не перегружен, то это:

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; }
} 
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...