分配前进行冗余比较和“如果”

The*_*exx 52 .net c# if-statement

这是示例:

if(value != ageValue) {
  ageValue = value;
}
Run Code Online (Sandbox Code Playgroud)

我的意思是,如果我们将一个变量的值分配给另一个变量,为什么还要检查它们是否具有相同的值?

这让我感到困惑。这里是更广泛的上下文:

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

  set {
    if(value != ageValue) {
      ageValue = value;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

Dmi*_*nko 50

这是检查非常有用的代码示例:

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

 ... 
Run Code Online (Sandbox Code Playgroud)

但是,更自然的实现是从一开始就进行检入,以避免不必要的计算。

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

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

      // ageValue has been changed
      // Time (or / and memory) consuming process
      SaveToRDBMS();
      InvalidateCache();  
      ...
    }
Run Code Online (Sandbox Code Playgroud)

  • 没错,但是我不认为“耗时(或内存消耗)的过程”是在设置员中要做的一件好事。毕竟,这就是为什么我们不能在属性上使用`async / await`的原因。请参阅[此问题的答案。](/sf/ask/462157111/)。 (6认同)
  • 就我个人而言,我会检查`if(value == ageValue)return;`。 (6认同)
  • @Guilherme 是的,但是,如果您正在实施 INotifyPropertyChanged 或相关的数据绑定处理程序,这种构造也非常有用。如果没有任何变化,您通常不希望触发更改跟踪代码。无论开销多么小,它仍然是不必要的,而且对于高频代码,很快就会增加。 (2认同)

Oli*_*ver 39

在winforms控件中,我们已将BackgroundColor设置为特定颜色:

myControl.BackgroundColor = Color.White
Run Code Online (Sandbox Code Playgroud)

在特定情况下,这可能会发生紧密循环,并导致UI冻结。经过性能分析后,我们发现此调用是UI冻结的原因,因此我们将其更改为:

if (myControl.BackgroundColor != Color.White)
    myControl.BackgroundColor = Color.White
Run Code Online (Sandbox Code Playgroud)

我们的工具的性能又回到了正轨(然后我们消除了紧密循环的原因)。

因此,此检查并不总是多余的。特别是如果目标是在设置器中执行更多操作的属性,则只需将值应用于后备存储即可。


Cod*_*dor 20

if检查,这不是多余的。这取决于其余的实现。请注意,在C#中,!=可以重载,这意味着评估可能会有副作用。此外,检查的变量可以实现为属性,这也可能对评估产生副作用。

  • 而且,如果您发现!=已超载,或者某人实施的属性带有非常不直观的副作用,请立即绘制并划分负责的开发人员。 (28认同)

Alo*_*aus 18

这个问题已经获得了很多评论,但是到目前为止,所有答案都试图重新构造该问题,以解决操作员超载或设置器副作用的问题。

如果setter被多个线程使用,则确实可以有所作为。如果要使用多个更改数据的线程迭代相同的数据,则设置前检查模式可能(应该测量)很有用。这种现象的教科书名称称为虚假共享。如果您读取数据并确认它已经与目标值匹配,则可以忽略写入。

如果忽略该写操作,则CPU无需刷新高速缓存行(Intel CPU上为64字节的块),以确保其他内核看到更改后的值。如果另一个内核要从该64字节块中读取其他数据,则您只是减慢了内核速度,增加了跨内核流量,以在CPU缓存之间同步内存内容。

以下示例应用程序显示了这种效果,其中还包含写前检查条件:

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

这是完整的代码:

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}");
    }
}
Run Code Online (Sandbox Code Playgroud)

如果运行该命令,则将获得如下值:

// 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
Run Code Online (Sandbox Code Playgroud)

这样可以使性能提高22%,这在高性能数字处理场景中非常重要。

要回答所写的问题:

如果仅单线程访问内存,则可以删除if语句。如果多个线程在同一个或附近的数据上工作,则可能发生错误共享,这可能使您损失多达约200美元。内存访问性能的20%。

更新1 我进行了更多测试,并创建了一个图表来显示跨核心聊天记录。这显示了一个简单的集合(无条件集合),评论者Frank Hopkins指出了这一点。条件未设置包含从不设置值的if。最后但并非最不重要的是,条件集将在if条件中设置值。

性能与核心

  • 如果setter被多个线程使用而没有任何同步,这实质上意味着生成的数据不一致,并且此时的处理速度实际上并不那么重要。C#不需要写入一个线程中的非易失性字段才能在其他线程中可见,直到引用了一个volatile字段,获取/释放了“锁”或创建/终止了线程为止。 (2认同)
  • 那是一个简化的例子。假设您有一个结构,其中有两个字段为int F1,F2。然后在两个线程中更新它们,其中一个仅更新F1,另一个则更新F2。写入数据没有重叠,但是由于错误共享,您的性能将下降。这意味着虚假共享。由于CPU的缓存如何工作,您对数据的依赖关系是错误的。 (2认同)
  • @Joker_vD:如果不同的线程正在写有意义的不同数据,那将是正确的。但是,在所有将要修改某些内容的线程都将存储相同值或等效值的情况下,可能会发生错误共享。例如,如果一个对象有一个字段来说明它是否曾经做过某事,则在将其设置为true时将其设置为true可能会降低其他尝试读取该标志的线程的性能。 (2认同)