这个问题的原因是什么导致了在 .Net 4 中出现如此巨大的性能差异?

18

我正在研究红黑树。我知道在.NET 4.0中,SortedSet类使用红黑树。因此,我使用Reflector将其部分取出并创建了一个RedBlackTree类。现在,我正在对这个RedBlackTree进行一些性能测试,并插入40000个连续的整数值(从0到39999),我惊讶地发现存在巨大的性能差异,如下:

 RBTree    took 9.27208   sec to insert 40000 values 
 SortedSet took 0.0253097 sec to insert 40000 values

什么是背后的原因?顺便说一下,我只在发布配置中运行了测试,这是小型测试代码。
            var stopWatch = new Stopwatch();
            var rbT = new RedBlackTree<int>();      
        stopWatch = new Stopwatch();
        stopWatch.Start();
        for (int i = 0; i < 40000; i++) {
            rbT.Add(i);
        }
        stopWatch.Stop();
        Console.WriteLine(stopWatch.Elapsed);

        var ss = new SortedSet<int>();
        stopWatch = new Stopwatch();
        stopWatch.Start();
        for (int i = 0; i < 40000; i++) {
            ss.Add(i);
        }
        stopWatch.Stop();
        Console.WriteLine(stopWatch.Elapsed);

编辑

我想分享我提取的RBTree代码,以便您也可以运行诊断

public class Node<T>
    {
        public Node(){}

        public Node(T value)
        {
            Item = value;
        }       

        public Node(T value, bool isRed)
        {
            Item = value;
            IsRed = isRed;
        }

        public T Item;
        public Node<T> Left;
        public Node<T> Right;
        public Node<T> Parent;
        public bool IsRed;
    }

    public class RedBlackTree<T>
    {
        public RedBlackTree(){} 

        public Node<T> root;
        int count, version; 
        Comparer<T> comparer = Comparer<T>.Default;     

        public void Add(T item)
        {
            if (this.root == null)
            {
                this.root = new Node<T>(item, false);
                this.count = 1;
                this.version++;
                return;
            }

            Node<T> root = this.root;
            Node<T> node = null;
            Node<T> grandParent = null;
            Node<T> greatGrandParent = null;
            this.version++;

            int num = 0;
            while (root != null)
            {
                num = this.comparer.Compare(item, root.Item);
                if (num == 0)
                {
                    this.root.IsRed = false;
                    return;
                }
                if (Is4Node(root))
                {
                    Split4Node(root);
                    if (IsRed(node))
                    {
                        this.InsertionBalance(root, ref node, grandParent, greatGrandParent);
                    }
                }
                greatGrandParent = grandParent;
                grandParent = node;
                node = root;
                root = (num < 0) ? root.Left : root.Right;
            }
            Node<T> current = new Node<T>(item);
            if (num > 0)
            {
                node.Right = current;
            }
            else
            {
                node.Left = current;
            }
            if (node.IsRed)
            {
                this.InsertionBalance(current, ref node, grandParent, greatGrandParent);
            }
            this.root.IsRed = false;
            this.count++;
        }


        private static bool IsRed(Node<T> node)
        {
            return ((node != null) && node.IsRed);
        }

        private static bool Is4Node(Node<T> node)
        {
            return (IsRed(node.Left) && IsRed(node.Right));
        }

        private static void Split4Node(Node<T> node)
        {
            node.IsRed = true;
            node.Left.IsRed = false;
            node.Right.IsRed = false;
        }

        private void InsertionBalance(Node<T> current, ref Node<T> parent, Node<T> grandParent, Node<T> greatGrandParent)
        {
            Node<T> node;
            bool flag = grandParent.Right == parent;
            bool flag2 = parent.Right == current;
            if (flag == flag2)
            {
                node = flag2 ? RotateLeft(grandParent) : RotateRight(grandParent);
            }
            else
            {
                node = flag2 ? RotateLeftRight(grandParent) : RotateRightLeft(grandParent);
                parent = greatGrandParent;
            }
            grandParent.IsRed = true;
            node.IsRed = false;
            ReplaceChildOfNodeOrRoot(greatGrandParent, grandParent, node);
        }

        private static Node<T> RotateLeft(Node<T> node)
        {
            Node<T> right = node.Right;
            node.Right = right.Left;
            right.Left = node;
            return right;
        }

        private static Node<T> RotateRight(Node<T> node)
        {
            Node<T> left = node.Left;
            node.Left = left.Right;
            left.Right = node;
            return left;
        }

        private static Node<T> RotateLeftRight(Node<T> node)
        {
            Node<T> left = node.Left;
            Node<T> right = left.Right;
            node.Left = right.Right;
            right.Right = node;
            left.Right = right.Left;
            right.Left = left;
            return right;
        }

        private static Node<T> RotateRightLeft(Node<T> node)
        {
            Node<T> right = node.Right;
            Node<T> left = right.Left;
            node.Right = left.Left;
            left.Left = node;
            right.Left = left.Right;
            left.Right = right;
            return left;
        }

        private void ReplaceChildOfNodeOrRoot(Node<T> parent, Node<T> child, Node<T> newChild)
        {
            if (parent != null)
            {
                if (parent.Left == child)
                {
                    parent.Left = newChild;
                }
                else
                {
                    parent.Right = newChild;
                }
            }
            else
            {
                this.root = newChild;
            }
        }
    }

编辑


我对其他一些数据结构进行了相同的诊断(一些是我创建的*,一些来自 .net 框架**),以下是有趣的结果

*AATree                 00:00:00.0309294
*AVLTree                00:00:00.0129743
**SortedDictionary      00:00:00.0313571
*RBTree                 00:00:09.2414156
**SortedSet             00:00:00.0241973

RBTree与上面介绍的SortedSet类相同(已经剥离出来)。我尝试使用400000个值,但是RBTree似乎需要很长时间,我真的不知道为什么。


1
当你说你在发布配置下“运行”测试时,是在调试器内部吗?还是在普通命令提示符下? - Marc Gravell
4
如果按相反的顺序执行代码,比如先执行SortedSet,再执行RBTree,这种情况是否仍然会发生? - ChrisBD
1
我怀疑是JIT的问题。像ChrisBD所说的那样,以相反的顺序运行它,或者运行多次,而不仅仅是一次(我的意思是将代码放在循环中,使其在同一程序执行中运行多次)。 - Chochos
我以相反的顺序和多次迭代运行了测试。但不幸的是,每次都得到相同的结果。 - Anindya Chatterjee
6
如果JIT需要9秒钟才能完成这个任务,我们就不会使用.NET。 - H H
显示剩余3条评论
4个回答

17

你的 Node<T> 类存在 bug。当你调用只有一个参数值的构造函数时,应该将 IsRed 设置为 true

我认为修复后的 Node<T> 类应该像这样:

public sealed class Node<T>
{
    public T Item { get; private set; }
    public bool IsRed { get; set; }
    public Node<T> Left { get; set; }
    public Node<T> Right { get; set; }

    public Node(T value)
    {
        Item = value;
        IsRed = true;
    }

    public Node(T value, bool isRed)
    {
        Item = value;
        IsRed = isRed;
    }
}

另一种选择 - 我的偏好 - 将是完全省略构造函数,并始终要求在实例化新节点时明确设置IsRed:
public sealed class Node<T>
{
    public T Item { get; private set; }
    public bool IsRed { get; set; }
    public Node<T> Left { get; set; }
    public Node<T> Right { get; set; }

    public Node(T value, bool isRed)
    {
        Item = value;
        IsRed = isRed;
    }
}

然后在您的 Add 方法中替换此行...
Node<T> current = new Node<T>(item);

通过这个...

Node<T> current = new Node<T>(item, true);

哦!你太棒了。我刚刚修改了它并运行了诊断,猜猜现在相同数据集的响应时间是多少 - 00.0204438秒。非常感谢你啊,你救了今天的大事。 - Anindya Chatterjee
太棒了。很多人包括我都在猜测NGen、反向排序、编译问题……看来它是源代码的问题 :) - Larry
在这种情况下,很难指出问题所在。通常更便宜的(脑海中首选)与环境有关。但是,在出现这种显著性能差异或其他类型的完全意外结果的情况下,我认为首先应该检查代码。我有类似的经验,通过针对代码进行调整和修正代码问题,总是可以解决问题! - Munish Goyal
这也是Occam剃刀原则的一个很好的例子。"它指出,在竞争的假设中,应选择具有最少假设的假设。" - Ali Akdurak

3
  1. 反转测试顺序并重复测量。
  2. 随机化你的数据。当你插入预排序数据时,排序集合的行为会变得奇怪。

1
你说得对!随机化数据可以给出可比较的结果。性能差异微不足道。但我在谈论最坏情况。为什么在这种特殊情况下,这两个表现不同,而SortedList是由RBTree本身实现的呢? - Anindya Chatterjee
所以它们是两个完全不同的算法。又是一次徒劳的追逐。 - H H

1

SortedSet 包含一个TargetedPatchingOptOut属性,你复制的版本中包含吗?

[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public bool Add(T item)
{
    return this.AddIfNotPresent(item);
}

1
这与性能无关。根据MSDN的说明,应用此属性的.NET Framework类库方法不太可能受到维护发布的影响,因此有资格在本机映像生成器(NGen)映像中进行内联处理。 - Anindya Chatterjee
我怀疑性能差异不是由于在本机映像生成器(NGen)映像之间进行方法内联造成的。 - dtb
@Anindya Chatterjee - 这些方法很小,因此将它们内联可以在性能方面发挥重要作用。 - Filip Navara

0

如果差距不是那么大,我会建议原因是.NET程序集已经被NGen编译成本地代码。对于你的类而言,将IL代码编译成本地代码所需的时间会在测试期间分摊。增加循环迭代次数会如何影响时间?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接