【问题标题】:The radix sort algorithm may have different run-time for datas with different order?基数排序算法对于不同顺序的数据可能有不同的运行时间?
【发布时间】:2015-11-26 00:37:00
【问题描述】:

我正在尝试实现基数排序算法。 我使用链表来存储要排序的元素,然后将每个元素扔到它的桶中。这个桶只是一个指针,它将链接属于它的桶的元素列表。

我正在测试区间 [0, 1000000] 中的 10.000.000 和 100.000.000 个整数。这些数字可以按新月、递减和随机顺序排列。

100.000.000 个以新月和递减顺序排列的数字的运行时间约为 20 秒。但是对于具有随机顺序的相同数量的元素 运行时间约为 110 秒。

据我了解,该算法对于任何质量都具有相同的复杂性 要排序的数据。

有人知道为什么会这样吗?

这是我的代码:

    void radix(Number** numbers)
    {
        unsigned int i, k, e = 1;
        Number* bucket[10];
        Number* tail[10];
        Number* index;

        for(k = 0; k < 7; k++, e *= 10)
        {
            for(i = 0; i < 10; i++) bucket[i] = tail[i] = NULL;

            index = *numbers;
            while(index != NULL)
            {
                i = (index->value / e) % 10;

                if(tail[i] == NULL)
                    bucket[i] = index;
                else
                    tail[i]->next = index;

                tail[i] = index;
                index = index->next; 
            }

            for(i = 0; i < 10; i++)
            {
                if(tail[i] != NULL)
                {
                    *numbers = bucket[i];
                    index = tail[i];
                    for(i++; i < 10; i++)
                    {
                        if(tail[i] != NULL)
                        {
                            index->next = bucket[i];
                            index = tail[i];
                        }
                    }
                }
            }

            index->next = NULL;
        }
    }

Number 在哪里:

typedef struct number
{
    unsigned int value;
    struct number* next;
} Number;

【问题讨论】:

  • 它可能具有相同的复杂性,但这并不意味着它具有相同的运行时间。
  • 但是如果是相同的数据,只是顺序不同,这会影响很大吗? 20 到 110 秒对于相同的数据来说太长了。
  • @X0R40 不,诸如分支预测失败(或缓存未命中,就此而言)之类的事情很容易仅通过重新排序数据就导致巨大的性能差异。
  • @IskarJarak 在看到您的评论后,我最终包含了一小部分关于分支错误预测的内容。它让我忘记了缓存未命中的焦点。希望你不要介意!
  • @Ike 我当然不介意!更好的答案就是更好的答案:)

标签: c algorithm sorting radix-sort


【解决方案1】:

答案可能与内存访问和引用位置有关。

升序/降序有一个规则的模式,可能具有更大的时间局部性,与其说是桶,但可能更多的是关于链表节点用于数字的方式(特别是如果它们是'不连续)。

例如,如果我们接受输入:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ...

我们从存储桶 0 循环到存储桶 9,然后再返回存储桶 0。当我们返回存储桶 0 时,我们正在访问一个 number 节点,该节点最近才被访问(仅 9 次迭代前),这很可能缓存到更快、更小的内存中。

如果我们使用随机排序,谁知道我们什么时候会回到存储桶 0?因此,在我们返回用于任何给定存储桶头部的数字的内存之前,我们更有可能长时间将数据从 DRAM 移动到缓存。结果可以转化为更多这样的以前的number 节点被从缓存中驱逐,当我们回到这样的存储桶时,更多的缓存未命中。

对于不规则排序,分支错误预测也可能会消耗一些时间。分析应该有助于缩小原因。

如果您确实存在内存瓶颈,可以尝试的一种方法是将您的存储桶变成,例如,您可以将数字深度复制到的展开列表。这将使您不再访问以前插入的数字的内存,这些数字可能已经插入了很多次迭代(由于随机排序,一个潜在的大变量)。有了这个,我们开始取回一些时间局部性(如果数字是连续分配的,可能还有空间局部性),否则我们将失去这种链表表示。然后它变成关于重用存储桶的连续内存(只有 10 个),而不是存储桶中的元素,它们之间的步幅可变。我们还通过展开的表示获得了桶内的空间局部性。

但是如果是相同的数据,只是顺序不同,这会影响一个 很多 ? 20 到 110 秒对于相同的数据来说太长了。

内存效率可以产生数量级的差异。 http://lwn.net/Articles/250967/

我不是这个主题的专家(更多的是“分析它并尝试根据指南进行优化”类型),但根据我过去从内存优化中获得的结果,我经常会将它们放在同等水平就算法优化的效果而言。例外情况是复杂性差异很大(例如:线性与二次),但如果前者对缓存更加友好,那么即使是线性算法也可以非常容易地击败具有非常大输入的线性算法。

【讨论】:

  • 抱歉,我不明白如何将我的存储桶变成“展开列表”以摆脱(可能)失败的分支预测或缓存未命中。
  • 有点像你的基本链表想法——但不是存储一个value,你可以存储,比如说,32个值,也许还有一个size变量,包含有多少数字被占用节点。然后将数组链接在一起。
  • 对于这类东西来说,这是一个非常有用的结构,它为您提供了链表的一些品质,以及我们通常使用数组获得的一些引用位置。
  • 计算机倾向于抓取少量较慢的内存并将它们缓存到更快的内存中。从一直到分页,例如,从二级存储(例如磁盘)到 DRAM 的 4 KB 块。 . 然后 DRAM 到缓存线的层次结构,一直到 64 字节的 L1 缓存线.. 然后到,比如说,一个很小的 ​​64 位 GP 寄存器。因为它以块的形式抓取所有这些内存,所以当您使用周围块中的内存时,事情往往会变得更快,然后再将其从更快的内存中逐出——这就是为什么数组往往非常好——我们可以通过一把一把,并使用周围的数据。
  • 这真的很简单。当内存中的连续节点最初以随机顺序包含数据时,当链接已更改以对数据进行排序时,遍历它们会以随机顺序接触内存。随机顺序不利于内存层次结构的所有级别的缓存性能。从连续到随机顺序的因子 6 还不错。你的机器必须有一个相当不错的内存子系统。
猜你喜欢
  • 2014-03-06
  • 2013-11-15
  • 2020-08-02
  • 1970-01-01
  • 1970-01-01
  • 2016-04-12
  • 1970-01-01
  • 2011-05-21
  • 1970-01-01
相关资源
最近更新 更多