AlphaDev将排序算法提速70%!C语言库作者一文详解DeepMind最新AI
新智元报道
新智元报道
【新智元导读】阿尔法家族新成员AlphaDev近来引发不少关注与讨论。一位曾在谷歌工作的研究人员对这项最新研究进行了详解。
几天前,DeepMind推出了AlphaDev,直接把排序算法提速70%。
这一全新AI系统,便是基于下棋高手AlphaGo打造。
而这项研究恰恰激起了前谷歌研究人员Justine Tunney的兴趣。
她表示,作为一名C语言库的作者,我一直在寻找机会来策划最好的东西。
一起看看Justine如何详解DeepMind排序算法。
DeepMind排序算法
DeepMind的这一发现赢得了当之无愧的关注,但不幸的是,他们本可以更好地解释AlphaDev。
接下来,从DeepMind发布的汇编代码开始,该代码将一个有三个项目的数组进行排序,从伪汇编翻译成汇编:
我将这个函数命名为 move37() ,是因为DeepMind的博客文章,将其与AlphaGo下的令人震惊的「第37步」进行了比较。
在2016那场人机大战中,AlphaGo下了一颗违反人类直觉的棋,一个简单的肩冲,击败了传奇围棋选手李世石。
所以如果运行DeepMind代码:
但是,在我看来这是一个错误。
我们给它的数组是{3,1,2},但 move37() 将其排序为{2,1,3}。
DeepMind一定在欺骗我们,因为我不相信2在1之前。再来看看他们对LLVM libcxx所做的开源贡献,这有望澄清一些事情:
所以 move37() 实际上不是一个排序函数,而是一个排序内核,旨在用作 sort3() 函数的构建块。
如果论文和博客文章能提到这一点就好了,因为它让我在最短的时间内感到非常困惑。下面是更好的代码版本,其中包括缺失的交换(swap)操作。
为了解释为什么他们的代码很重要,让我们考虑一下这个算法在高层次上是如何工作的。当我第一次尝试自己解决 sort3() 问题时,我想到了这个:
然后我查看了libcxx,发现它们也在做同样的事情。上述代码的问题是,编译器并不善于优化它。
如果你尝试编译上面的代码,就会注意到你的编译器插入了大量的分支指令。这就是DeepMind试图通过LLVM贡献来改进的地方。
然而,这些技术往往不太容易理解。
我实际上喜欢天真无邪的代码,因为如果我们眯起眼睛,可以看到一种模式,与DeepMind最先进的汇编代码有相同的基本想法。
这个想法是这个问题本质上归结为3个比较和交换操作:
上面的代码是之前排序网络的最先进技术。现在,这就是DeepMind的新发现发挥作用的地方。他们发现有时上面的 mov 指令是不必要的。
如果你试着运行上面的代码,你会发现不管有没有被删除的行,它都是100%正确的。
这行代码看起来像是在做什么,但实际上什么也没做。所以我并不惊讶这样的事情会被计算机科学忽视几十年。
现在也应该更清楚AlphaDev是如何工作的。
DeepMind基本上构建了一个人工智能,它可以摆弄汇编代码,随机删除一些东西,看看它是否损坏。
我这么说并不是要否定AlphaDev的智能,因为如果我说我没有做同样的事情,那就是在撒谎。
上面的代码中还有两个 mov 指令,我们有可能将其删除。通过使用ARM64指令集来做到这一点,它可以为类似的问题提供更小的代码。
在这里,我们不需要任何指令来创建临时变量:
Arm公司最近风头正劲,我想上面的例子可以作为他们赢得名声的证据。
Arm也是目前开源领域最好的公司之一。比如,他们的MbedTLS库是我迄今为止见过的最被低估的瑰宝之一。
当我开始使用它时,我原本有这样的计划,即修改Arm的代码,使之在x86硬件上更好地工作。
我编写了所有这些精心设计的汇编优化,使其与x86上的OpenSSL达到相同的性能。
MbedTLS是简单、可移植、可破解的C代码,因此对于任何想要一个不是Perl生成的汇编的加密库的人来说,是个好消息。
我告诉了Arm公司的人我在做什么,他们并没有觉得这是颠覆性的。
我希望有一天能找到时间做DeepMind做的事情,并在上游进行修改。Arm公司的优化程序库也是多产的,它在质量上与双转换无懈可击。
它对C库对此特别感兴趣,因为几十年来,开源社区一直依靠Sun Microsystems在90年代初编写的数学函数来维持生计。
Arm找到了一种改进其中几个函数的方法,例如 pow(x,y) 。考虑到这是数学中最基本的运算之一,这是一件非常有影响力的事情。
比如,如果你在纯软件中使用Arm的解决方案在x86机器上实现 pow(x,y) ,那么它将比英特尔的原生x87指令快5倍。
很幸运,DeepMind也加入了这个游戏,所以我冒昧地把他们的libcxx diff翻译成可读的C代码。
这是我希望在论文和博客文章中看到的另一件事,因为在这段代码中,你会发现专家们用来让编译器生成无分支 MOVcc 指令的规范技巧。
当我看到 Sort5() 函数,我觉得自己对DeepMind研究的动机有了更好的理解。
如果你在ARM64上编译 Sort5() 函数,那么编译器将产生一个处理11个寄存器的函数。如果你在推理一个数学方程,那么你能一次在你的工作记忆中保存11个变量吗?
可能不会。这就是为什么有一个像 PartialSort3 这样优秀的内核函数如此有用的原因。
值得一提的是, Sort3() 和 Sort5() 本身就是内核,因为它们旨在成为传统排序功能的构建块。
博客文章涵盖了这个主题,但我认为分享一些实际上可移植和可执行的东西会很有用。
The above algorithm shows what the new and improved libcxx is doing. It's basically quicksort except it switches to the sorting kernels and insertion sort when recursing into smaller slices. With libcxx I think they even took the added step of schlepping in heapsort, which is kind of slow, but prevents adversaries from smashing your stack. 上面的算法显示了新的和改进的libcxx正在做什么。它基本上是快速排序,除了在递归到更小的切片时切换到排序内核和插入排序。对于libcxx,我认为他们甚至采取了在堆排序中移动的额外步骤,这有点慢,但可以防止对手破坏您的堆栈。
微信扫码关注该文公众号作者