c# - 分配前的冗余比较和“if”




.net if-statement (5)

以下是检查 非常有用 时的代码示例:

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

这是一个例子:

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

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

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

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

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

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

主要的是,如果 引用处 的值在逻辑上等于先前引用的值,则不设置 新引用 。 在上面的评论中,用户批评了这种情况的讨厌 - 并且必须处理它 令人讨厌的 - 但在案件中仍然是必要的。

我试着分割这样的用例:

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

    • 这在数学程序中经常发生,例如Mathematica,你不能使用原始数字,允许你最终得到代表相同的不同对象。
  2. value 的引用对于缓存逻辑很有用。

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

    • 具体如何以及为何如此重要因具体情况而异。

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

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

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

对于另一个随机的例子, .NET的垃圾收集器是“ 世代的 ,这意味着它会更加努力地检查一个值是否可以在更新时收集。 因此,如果您优先保留旧引用,垃圾收集器可以获得收益,因为它位于更具特权的生成中,允许较新的引用更快地收集垃圾。

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

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


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


性能不是很大,只取决于您的逻辑需求。


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

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

如果省略写入,则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 我已经运行了更多测试并创建了一个图表来显示跨核心聊天。 这显示了一个简单的集合( 无条件集 ),正如评论者弗兰克霍普金斯所指出的那样。 条件未设置 包含从不设置值的if。 最后但并非最不重要的 条件集 将在if条件中设置值。





if-statement