【问题标题】:Redundant comparison & "if" before assignment分配前的冗余比较和“如果”
【发布时间】:2019-08-13 11:13:41
【问题描述】:

示例如下:

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

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

这让我很困惑。以下是更广泛的背景:

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

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

【问题讨论】:

  • 假设 ageValue 不是一个属性,而只是一个变量,那么没有意义。如果ageValue 是一个属性,那么集合中可能发生了什么?
  • 所以只有当值是新值时才设置ageValue。在这里,拥有if 是没有意义的,但在其他情况下,当我们做的不仅仅是设置值时,它可以节省时间。例如,在 ageValue = value 之后的 WPF MVVM 应用程序中,我们很可能会调用 NotifyPropertyChanged,因此 GUI 知道属性已更改,但我们只想在属性实际更改时才这样做
  • 关于您添加/编辑的代码:是的,if 是多余的在您给出的这个特定代码示例中。跨度>
  • @SeM:它得到了支持,因为最荒谬的琐碎问题通常是最受欢迎的。不幸的是,所有 stackexchange 都是如此,因为对于任何主题,初学者都比专家多得多。

标签: c# .net if-statement


【解决方案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();  
      ...
    }

【讨论】:

  • 是的,但是我不认为“消耗时间(或/和内存)的过程”在 setter 中是一件好事。毕竟,这就是为什么我们不能在属性上使用async/await。见the answer of this question.
  • 就我个人而言,我的支票是if (value == ageValue) return;
  • @Guilherme 是的,但是,如果您正在实现 INotifyPropertyChanged 或相关的数据绑定处理程序,此构造也非常有用。如果没有任何变化,您通常不想触发更改跟踪代码。不管开销多么小,它仍然是不必要的,并且对于高频代码,很快就会增加。
【解决方案2】:

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

myControl.BackgroundColor = Color.White

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

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

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

所以这个检查并不总是多余的。特别是如果目标是在 setter 中执行更多操作的属性,那么只需将值应用于后备存储。

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 如果你发现 != 已经超载,或者有人实现了带有非常不直观的副作用的属性,请立即画出负责的开发人员并将其分成四份。
    • @BobbyA:副作用不一定是不直观的,只需expensive
    【解决方案4】:

    这个问题已经获得了相当多的 cmets,但到目前为止,所有答案都试图重新构建问题以解决运算符重载或 setter 副作用的问题。

    如果 setter 被多个线程使用,它真的会产生影响。如果您使用多个更改数据的线程迭代相同的数据,则设置模式之前的检查可能(您应该测量)很有用。这种现象的教科书名称称为false sharing。如果您读取数据并确认它已经与目标值匹配,则可以省略写入。

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

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

     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 语句。如果多个线程正在处理相同或附近的数据,则可能会发生错误共享,这可能会导致您高达 ca. 20% 的内存访问性能。

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

    【讨论】:

    • 如果 setter 被多个线程使用而没有任何同步,这本质上意味着生成的数据不一致,此时处理速度并不是那么重要。 C# 不要求对一个线程中的非易失性字段的写入在其他线程中可见,直到引用了易失性字段,或者 lock 被占用/释放,或者线程被创建/终止。
    • 这是一个简化的例子。假设您有一个包含两个字段 int F1、F2 的结构。然后你在两个线程中更新它们,一个只更新 F1,另一个更新 F2。写入的数据没有重叠,但由于错误共享,您的性能会下降。这意味着虚假共享。由于 CPU 缓存的工作方式,您有错误的数据依赖性。
    • @Joker_vD:如果不同的线程正在写入有意义的不同数据,那将是真的。但是,如果所有修改某些内容的线程都存储相同的值或等效值,则可能会发生错误共享。例如,如果一个对象有一个字段来说明它是否曾经做过某事,那么将标志设置为true 可能会降低尝试读取标志的其他线程的性能。
    • hmm,示例代码实际上比较了对每个元素执行三个操作(检索值、比较值、递增计数器)与对每个元素执行四个操作(检索、比较、设置、递增)。自然后者必须更慢(如果每个操作花费大致相同的时间,大约 25%...)。一个更具指示性的比较是有检查的代码与根本没有检查的代码。
    • @FrankHopkins:我创建了一个图表并稍微更改了代码以显示无条件集。有趣的旁注:在 .NET Core 3.0 Beta 上运行它的性能要差得多。看起来这仍然是一个调试版本,或者至少启用了许多检查。
    【解决方案5】:

    实际上,出于不同的原因,我曾多次编写过类似的代码。它们有点难以解释,所以请耐心等待。

    主要的是,如果引用处的值在逻辑上等于先前引用的值,则不要设置新引用。在上面的 cmets 中,用户批评了这种情况的令人讨厌 - 而且它 不得不处理 - 但在某些情况下仍然必不可少。

    我会尝试像这样拆分用例:

    1. 该值是一种抽象数据类型,您可能有不同的构造实例表示相同的逻辑值。

      • 这在数学程序中经常发生,例如在 Mathematica 中,您不能使用原始数字,因此您可以使用不同的对象来表示相同的对象。
    2. value 的引用对缓存逻辑很有用。

      • 在使用抽象数字时也可以弹出。例如,如果您希望程序的其他部分缓存有关引用的数据,那么您不希望将其替换为逻辑上等效的引用,因为它会使其他地方使用的缓存无效。
    3. 您正在使用反应式评估器,其中设置新值可能会强制更新的链式反应。

      • 这件事的具体方式和原因因具体情况而异。

    重要的概念是,在某些情况下,您可以将相同的逻辑值存储在不同的引用中,但您希望尽量减少退化引用的数量,原因有两个:

    1. 多次存储相同的逻辑值会占用更多内存。

    2. 很多运行时都可以使用引用检查作为快捷方式,例如通过缓存,如果您避免传播对相同逻辑值的冗余引用,则效率会更高。

    对于另一个随机示例,.NET's garbage collector is "generational",这意味着它会花费更多精力来检查是否可以收集较新的值。因此,如果您优先保留较旧的引用,垃圾收集器可以获得收益,因为它位于特权更高的一代中,从而允许较新的引用更快地进行垃圾收集。

    另一个用例,同样是抽象数据类型,是您可能附加了惰性求值属性的地方。例如,假设您有一个abstract class Number,它具有.IsRational.IsEven 等属性。然后,您可能不会立即计算这些,而是​​按需生成它们,缓存结果。在这种情况下,您可能倾向于保留具有相同逻辑值的旧 Number,因为它们可能附加了更多的东西,而新的 value 可能有较少的相关信息,即使逻辑上是==

    很难想出如何总结为什么这在某些情况下有意义的各种原因,但它基本上是一种优化,如果你有理由使用它就有意义。如果您没有任何理由使用它,那么最好不要担心它,直到出现一些动机。

    【讨论】:

      【解决方案6】:

      性能不是什么大问题,只是取决于你的逻辑需求。

      【讨论】:

        【解决方案7】:

        是的,这个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; }
        } 
        

        【讨论】:

        • 如果它们是具有执行其他操作的 getter 和 setter 的属性,那它没有用吗?
        • 它的 setter 方法(看问题)。不能有任何其他动作。唯一的事情(但并不经常发生)是一个重载的!=-operator。
        • 是的,这个问题已经扩展了额外的上下文。
        • 为什么是解决 OP 实际问题的唯一答案,而不是“如果”如此严重否决?
        • 因为在问题的第 4 版之前,这个答案是错误的。 Codor 的回答是唯一正确的。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-06-08
        相关资源
        最近更新 更多