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

时间:2019-03-22 12:55:25

标签: c# .net if-statement

这里是示例:

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

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

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

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

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

7 个答案:

答案 0 :(得分: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(); 
        ...
      } 
    } 

 ... 

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

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

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

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

答案 1 :(得分:39)

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

myControl.BackgroundColor = Color.White

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

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

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

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

答案 2 :(得分:20)

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

答案 3 :(得分:18)

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

如果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语句。如果多个线程在同一个或附近的数据上工作,则可能发生错误共享,这可能使您损失多达约200万美元。内存访问性能的20%。

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

Performance vs Cores

答案 4 :(得分:1)

出于不同的原因,我实际上已经对这种东西进行了几次编码。他们有点难以解释,所以请忍受我。

主要是,如果参考值在逻辑上等于先前参考值,则不要设置 新参考 。在上面的评论中,用户批评了这种情况的令人讨厌的地方-必须处理它 是令人讨厌的-但在某些情况下仍然是必需的。

我会尝试分解以下用例:

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

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

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

    • 具体如何以及为什么如此重要取决于上下文。

从概念上讲,重要的一点是,在某些情况下,您可以将相同的逻辑值存储在不同的引用中,但是出于两个重要原因,您想尝试尽量减少简并引用的数量:

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

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

对于另一个随机示例,.NET's garbage collector is "generational",这意味着它会花更多的精力检查值是否可以在更新时被收集。因此,如果您优先保留较旧的引用,则垃圾收集器会有所收获,因为它具有更高的特权,可以让较新的引用更快地收集垃圾。

另一个还是抽象数据类型的用例是您可能在其中附加了延迟评估的属性。例如,假设您有一个abstract class Number,其属性如.IsRational.IsEven等。那么,您可能不会立即计算出这些值,而是按需生成它们,并缓存它们。结果。在这种情况下,您可能倾向于保留逻辑值相同的旧Number,因为它们可能附加了更多内容,而新value可能具有较少的关联信息即使它在逻辑上是==

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

答案 5 :(得分:0)

性能并不重要,仅取决于您的逻辑需求。

答案 6 :(得分:-1)

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