下载本文的 PDF 版本 PDF

通过代码生成 DSL 进行设计探索

用于底层编程的高级 DSL


Bo Joel Svensson,印第安纳大学
Mary Sheeran,查尔姆斯理工大学
Ryan Newton,印第安纳大学


DSL(领域特定语言)使程序更短且更易于编写。它们可以是独立的——例如,LaTeX、Makefiles 和 SQL——或者它们可以嵌入到宿主语言中。您可能会认为,嵌入到高级语言中的 DSL 会是抽象的或数学导向的,远离底层编程的具体细节。事实并非如此。本文演示了高层 EDSL(嵌入式 DSL)如何真正地简化底层编程。这并不矛盾。

关于 EDSL 的温和介绍可以在本系列的前一篇文章中找到:“领域特定语言和使用 Haskell 的代码合成,”其中 Andy Gill 考虑了实现深度嵌入式 DSL 与独立编译器的优缺点。本文继续讲述,提出了一个稍微不同的问题:如果您需要在 C 或 CUDA 等语言中生成高性能、底层代码,那么使用(或创建)代码生成 EDSL 是否值得,或者您应该直接埋头手动编写底层代码?我们的答案是,EDSL 很可能是一个合理的选择。


为什么要生成高性能程序?

2010 年,本文的一位作者的任务是将高性能基准测试移植到新的计算机架构上运行。其中一个基准测试是旨在利用向量指令6的并行排序——确切地说是 SSE(流式 SIMD 扩展)指令。为了实现向量化,一旦数组达到很小的尺寸,这种排序就会降级为双调排序网络。像这样的排序网络可以直接向量化。当然,这类基准测试在代码中并没有维护干净抽象的声誉。实际上,这个特定的基准测试具有硬编码的排序网络,输入分别为 16、32 和 64,使用了许多页重复且难以理解的代码

...
xmm10 = _mm_min_ps(xmm1, xmm6);
xmm11 = _mm_max_ps(xmm1, xmm6);
xmm12 = _mm_min_ps(xmm2, xmm5);
xmm13 = _mm_max_ps(xmm2, xmm5);
xmm14 = _mm_min_ps(xmm3, xmm4);
xmm15 = _mm_max_ps(xmm3, xmm4);
...

程序的这种表示方式具有明显的缺点。首先,如何将其移植到具有不同向量指令的新架构?这是不可能的——您必须编写一个新版本。然而,浏览一下 双调排序器维基百科条目,就会发现该算法是一个简单而优雅的递归函数。为什么不能捕获这种本质结构,从而消除示例中的重复?至少可以使用 C 预处理器宏来减少重复

#define SORT16(a,b,c,d,e,f,g,h,i,j,k,l,m,n,p,q,r,s,t,u,v,w,x,y,z,za) \
 i = _mm_min_ps(a,h); \
 j = _mm_max_ps(a,h); \
 ...

SORT16然后可以重用以定义SORT32。尽管如此,C 预处理器宏并不能提供必要的编译时可编程性。相反,最佳解决方案是编写一个简单的程序生成器脚本,将上面的重复代码作为输出打印到文件中。

这里的教训是,软件工程师不应犹豫编写生成程序的程序。如果您可以识别程序中的规律性,那么程序生成可能是一个选择。有很多成熟的技术可以帮助实现这种元编程(MetaOCaml、Scheme 宏、Template Haskell、C++ 模板元编程等)。实际上,由于此处讨论的 DSL 是嵌入式的,因此它们的主机程序实际上是程序生成器(即元程序)。然而,这些 DSL 也施加了额外的结构,从而带来了多种好处。


DSL 提供安全性

前面提到的脚本非常原始;它直接以字符串形式发出每个生成的程序。EDSL 则为 DSL 程序发出抽象语法树,有了它,您可以做任何您想做的事情(有关更多详细信息,请参阅 Gill 的文章)。实际上,如果结构良好,用于生成代码的 EDSL 可以在生成的代码中提供安全保证;一个类型良好的 EDSL 程序应该保证类型良好的生成代码。这是尽早发现错误的好方法。例如,如果 EDSL 嵌入在 Haskell 中,Haskell 类型检查器会发现可能导致底层程序崩溃的错误。通过调试底层生成的代码来查找这些错误可能会慢得多——例如,在构建和调试时间较长的嵌入式系统中。

您可以更进一步,将 DSL 限制为特定的习惯用法,以获得额外的安全属性。例如,您可以保证只生成内存安全的程序,就像 Galois Inc. 在其 Ivory DSL 中生成 C 代码一样 (http://smaccmpilot.org/languages/)。


DSL 使编译器更智能

如今,用于高性能的 DSL 专注于并行性,但是并行实现细节向用户公开的程度在不同的 DSL 之间有所不同。有些 DSL,例如 SQL 和非常高级的数组 DSL2,3,4,11,16,21,22,隐藏了除抽象数据转换之外的所有内容,这些转换自然是并行的。这些编译器可以通过限制并行代码区域之间允许的通信类型,强制执行结构化数据处理模式(例如mapreduce),并消除使自动并行化变得困难的功能(例如,别名、指针和任意控制流),从而实现良好的性能。通过利用这些限制,您通常可以应用彻底的代码转换。这方面的一个例子是融合,即消除中间数据结构(例如,替换map f (map g arr)map (f ○ g) arr).

)。当这种方法效果良好时——也就是说,当编译器可以将声明式规范中的潜在并行性有效地映射到目标架构时——这是一个有吸引力的选择。但是,有时需要进行实验才能找到与目标匹配的良好并行分解,目标可能是 GPU(图形处理器)或 FPGA(现场可编程门阵列)等。然后,用户不仅希望对生成的代码进行精细控制,还希望能够轻松地更改它。例如,安迪·吉尔在前面提到的文章中描述的硬件描述语言 Kansas Lava 明确旨在用于设计空间探索,并且前向纠错器的案例研究表明,这如何带来高性能设计。在更高的抽象级别上,Bluespec (http://www.bluespec.com/high-level-synthesis-tools.html) 行为语言允许算法专家生成高性能 FPGA 加速器。一个主要的卖点是,它可以快速轻松地进行架构探索,特别是使用高度参数化的 DSL 代码。

这些相同的优势也适用于底层软件领域以及硬件。总的来说,并非所有的性能调整和优化都可以自动化,这就需要 DSL 来实现系统的用户引导的性能探索。


DSL 抽象代码生成策略

我们应该如何让程序员控制生成的代码,并提供简单的方法来改变它?由于 EDSL 程序在程序生成时执行任意计算,因此它们还可以以常规函数和数据类型的形式封装可重用的代码生成策略。让我们看一个例子:延迟数组表示1,5,7,10,它表示生成元素的能力,而不是已经驻留在内存中的元素。(在这方面,它们类似于许多语言中发现的生成器迭代器的概念,但具体细节却大相径庭。)

为了解释延迟数组,让我们从一个使用mapreduce之前函数

let arr2 = map f arr1
   sum = reduce (+) arr2
   prd = reduce (*) arr2
...

的简单程序开始。是否arr2需要内存来存储(因此,需要内存流量来读取和写入它)?在大多数传统语言中,答案是不折不扣的“是”。在高级数组 DSL 中——它们可能使用也可能不使用延迟数组——融合优化可能会消除中间数组。这是可能的,因为在许多这些 DSL2,4,11,16中,数组是不可变的,并且 DSL 本身甚至可能是无副作用的,这使得编译器很容易识别融合机会。因此,通常一个map操作可以融合到一个reduce操作中,这意味着最终为reduce创建的(并行或顺序)循环包含map“d 函数在该循环内部。

然而,在示例中,arr2被使用了两次。大多数数组 DSL 会选择不内联arr2,如果这意味着重复工作,或者他们将使用基于f的静态成本估计的启发式方法来决定工作重复是否值得以启用融合。另一方面,显式延迟数组将此决定留给程序员。它们根据生成数组的计算来表示数组,并支持默认融合方法。map在前面的示例中将被复制并融合到每个reduce中,除非用户通过应用一个名为force的函数显式地使数组在内存中“真实”。此外,延迟数组在嵌入式 DSL 上下文中获得了额外的优势:用于表示数组的函数永远不会遭受函数调用开销,因为它们在 EDSL 主机程序执行代码生成时会被内联。

延迟数组数据类型是用友好的 API 封装代码生成策略的一个示例。它们为程序员提供高级数组操作,同时保留对内存使用的控制。当从提供的库中组合已知的生产者和消费者函数时,它们还可以帮助避免无风险的边界检查(例如,mapreduce)。在后面的章节中,我们将描述一种提供更多控制的语言,其中不同的控制流模式封装在延迟数组的不同变体(推和拉)中。最后,本文展示了如何使用延迟数组使诸如map之类操作高度通用(例如,一个map操作,根据其应用上下文,生成顺序循环并行循环,并且如果是并行的,则可以在分层并行架构内的多个尺度之一上运行,在每种情况下生成非常不同的代码)。


DSL 可以混合搭配(设计探索)

最后一点——高级数据运算符可以根据它们使用的上下文生成不同的代码——更接近设计探索的目标。DSL 还可以通过哪些其他方式允许程序员探索不同的实现策略,同时更改的代码行数比他们必须手动转换生成的代码要少得多?一种方法是利用程序生成来构建高度参数化的设计。在硬件综合语言(例如 Bluespec 或 Lava)中,人们希望设计是灵活的,并且不局限于特定的电路区域。同样,在本文的后面,您将看到 GPU 程序,这些程序是根据要使用的 GPU 硬件线程数进行参数化的,有时在线程数不足时会更改生成的程序结构(注入顺序循环)。这种程序结构的即时确定在手写代码中很难实现。有了它,我们可能会在主机程序中接受 GPU 程序的列表,并配置 GPU 以连续且同时运行所有这些计算,每个程序都适应分配给它的线程数。

最后,随着设计探索能力的增强,一个常见的愿望是通过生成程序的许多变体、测试所有变体并选择获胜者来进行自动调优。一些 DSL 在内部执行这样的自动调优过程8,14,15,但这超出了本文的范围。相反,本文旨在帮助您权衡生成底层代码的嵌入式 DSL 的优缺点。为此,本文通过一个具体的例子来介绍:Obsidian,一个为 GPU 编程生成 CUDA 的嵌入式 DSL。首先,让我们回顾一下 CUDA 编程模型,然后再在本篇文章的剩余部分描述 Obsidian。


CUDA 入门

CUDA 是 NVIDIA 提供的用于 GPU 编程的 C 方言。(有 CUDA 先前经验的读者可以跳过本节。)

GPU 最初是用于生成计算机图形的硬件加速器,但已变得越来越通用,这既受到希望获得更高灵活性的图形程序员的推动,也受到新一代程序员的推动,他们的动机不是图形,而是加速非图形应用程序的数据并行部分。

包含支持 CUDA 的 GPU 的典型计算机系统分为两部分:主机,指的是计算机的 CPU 和主内存;以及设备,它由 GPU 及其关联的内存组成。设备通常以 PCI Express 卡的形式出现,其中包含 GPU 和内存。GPU 由多个 MP(多处理器)组成,每个 MP 包含一些功能单元(NVIDIA 术语中的 CUDA 核心)。每个 MP 还包含本地共享内存,可以将其视为程序员管理的缓存。MP 内的 CUDA 核心可以通过此共享内存进行通信。


大规模并行性

GPU 能够管理大量正在运行的线程。当数千个线程(执行几乎相同的工作)一起启动时,它会蓬勃发展。它具有管理所有这些线程的分层结构。在每个 MP 上启动多个线程,每个块最多包含 1,024 个线程。块内的线程(并且仅限于块内)可以使用 MP 的共享内存进行通信。启动到 GPU 上的块集合称为网格。块内 32 个连续线程的组称为线程束。线程束以锁步方式执行;所有线程在任何给定时刻都执行相同的指令。线程束中不参与计算的线程将被关闭。


可扩展架构

GPU 可能包含少至一个或多达 15 个 MP。CUDA 程序应该能够在沿规模的任何 GPU 上运行。CUDA 编程模型反映了 GPU 层次结构。程序员编写一个 SPMD(单程序多数据)程序,该程序由块内的线程执行。此程序的许多实例也在启动的所有块中执行。所有实例都是独立的,并且可以按任何顺序启动,或者跨可用 MP 并行启动。


所有这些的最终结果是,GPU 编程实际上与使用两个、四个或八个内核的多核机器编程不太相似。程序员需要启动数千个线程,并弄清楚它们应该如何在 GPU 的约束下进行通信。这意味着他们需要考虑线程束、块和网格,无论喜欢与否。(但是,正如将要看到的,DSL 仍然可以更容易地将抽象数据转换映射到层次结构。)

本文末尾的参考文献包括指向有关 CUDA 编程和 GPU 架构的进一步阅读的链接。12,13


Obsidian 语言

在 CUDA 中,并行for循环是隐式的;计算是在元素级别描述的。要访问哪些元素(在哪里加载和存储数据)表示为线程身份的函数——也就是说,线程必须询问“我在哪里?”并通过使用blockIdx, blockDimthreadIdx进行计算来回答问题。相比之下,Obsidian 程序在聚合级别描述数组到数组的计算,用集体操作替换索引运算。例如

vecAdd v1 v2 = zipWith (+) v1 v2

这个vecAdd函数接受两个数组,v1v2,作为输入,并使用zipWith库函数(zipWith只是一个map在两个数组上;有关库函数的选择,请参见图 1)执行逐元素加法。vecAdd但不是一个完整的 Obsidian 程序;需要有关如何将此程序映射到 GPU 层次结构的信息,这些信息将在程序的类型中提供。

Design Exploration through Code-generating DSLs: A Selection of Operations on Pull and Push Arrays

为了给诸如vecAdd之类的操作提供精确的类型,需要理解第一节中描述的延迟数组。具体来说,有数组变体。vecAdd程序在拉数组上运行。它的类型是

vecAdd :: Pull EFloat -> Pull EFloat -> Pull EFloat

也就是说,vecAdd接受两个EFloat值的拉数组作为输入,并返回一个拉数组。(类型前缀为E的值实际上是表达式树,如下一节所述。)拉数组实现为长度加上从索引到值的函数

type Pull a = (Size, (EWord32 -> a))

数组的这种表示形式免费提供了操作融合。例如,对于拉数组的上述定义,以下等式显示了映射到拉数组上的函数实际上是如何被吸收(组合)到它已经使用的基于函数的表示形式中的

map f arr == map f (sz,g) == (sz, f ○ g)

没有中间数据写入到内存中用于g的输出,但在某些情况下,将元素写入内存是可取的——例如,重新审视第一节中的示例,但现在假设在以下代码中f2很昂贵

let arr2 = map f2 arr1
   sum = reduce (+) arr2
   prd = reduce (*) arr2
...

在这里,arr2被使用了两次,程序员应该有权确保arr2被计算并完全存储在内存中。这就是force函数发挥作用的地方

do arr2 <- forcePull (map f2 arr1)
  let sum = reduce (+) arr2
      prd = reduce (*) arr2
  ...

切换到 Haskell 的do表示法的原因是forcePull是一个单子函数。Monad 是 Haskell 编码副作用的方式,在此程序中,所讨论的 monad 是 Obsidian 的Programmonad,它大致对应于 CUDA 代码。forcePull的返回类型是Program t (Pull a),其中t是一个参数,用于指定 GPU 层次结构中的级别(Thread, Warp, Block,Grid).

)。使用Programmonad,我们也可以定义Push数组

type Push t a = (Size, ((a -> EWord32 -> Program Thread ()) -> Program t ()))

与拉数组一样,推数组由长度和一个函数组成。不同之处在于,推数组中的函数(填充函数)会生成一个程序,该程序在层次结构中的t级别编码迭代模式。填充函数的参数本身是一个函数(接收器函数)。拉数组满足对特定元素的请求,而推数组仅允许批量请求以通过接收器函数推送所有元素。图 2 中描绘了这种关系。调用时,填充函数会创建循环结构,但它会将接收器函数的代码内联到该循环中。一个推数组,其元素由f计算,接收器为rcv,可能会生成for(i∈[1,N]) { rcv(i,f(i)); }

Design Exploration through Code-generating DSLs: The Interface Between Push/Pull Arrays and Their Clients

更具体地说,当强制将推数组写入内存时,每次调用rcv都会写入一个内存位置,A[i] = f(i).

Obsidian 同时具有拉数组和推数组的原因与性能有关。某些操作很容易在拉数组上表达并产生良好的代码,例如map, zipWith,和置换。然而,诸如append之类的操作,它接受两个拉数组并将它们连接起来,会在查找函数中导致条件语句。当强制执行此类连接数组时,此条件语句将在生成的for循环的每次迭代中执行(或由每个线程执行)。另一方面,推数组编码了它们自己的迭代模式,因此,当连接推数组时,它们的固有循环模式按顺序执行,而每个循环在每次迭代中都不会执行条件语句,而不是执行条件语句。

推和拉转换

(对于推数组)和force(对于拉数组)的结果始终是拉数组。这意味着可以通过应用forcePull将推数组转换为拉数组。但是,应用force总是会在内存中产生开销,并且它会实现计算所有元素的全部成本。force反方向转换——从拉数组转换为推数组——需要一个名为

push的函数。此函数没有任何直接相关的成本。它只是更改表示形式(尽管切换到推表示形式意味着失去仅计算数组部分的能力)。但是请注意,拉数组没有像推数组那样的参数——它们与层次结构级别无关。在拉数组上使用t并固定结果的的函数。此函数没有任何直接相关的成本。它只是更改表示形式(尽管切换到推表示形式意味着失去仅计算数组部分的能力)。但是请注意,拉数组没有像推数组那样的参数会将数组锁定到给定的层次结构级别,如下所示tvecAdd :: Pull EFloat -> Pull EFloat -> Push Grid EFloat

vecAdd v1 v2 = push (zipWith (+) v1 v2)
现在,

程序已完成vecAdd将结果数组转换为推数组,并且结果类型固定为的函数。此函数没有任何直接相关的成本。它只是更改表示形式(尽管切换到推表示形式意味着失去仅计算数组部分的能力)。但是请注意,拉数组没有像推数组那样的Push Grid,因此迭代变为级别并行。Grid设计空间探索


即使是像归约这样简单的操作,也需要设计空间探索才能在 GPU 上获得良好的性能。每个线程的顺序归约可以与块中多个线程上的并行二叉树形归约相结合。顺序工作的程度和用于并行工作的线程数都可以变化。本节介绍了一种局部(片上、共享内存)

算法。它对每个 GPU 块执行一次归约。这可以用作所有 GPU 线程上的全尺度归约算法的构建块。reduce以下代码实现了递归并行归约。在每个步骤中,输入数组在中间拆分,并将两个结果数组中的元素成对相加,从而产生长度减半的中间数组。中间数组使用

强制写入内存,在这种情况下,在forcePull级别使用每个元素一个线程(在Block级别)。

reduceLocal :: Scalars a => (a -> a -> a) -> Pull a -> Push Block a
reduceLocal f arr = singletonPush (loop arr)
 where loop arr | len arr == 1 = return (arr ! 0)
                | otherwise =
                    do let (a1,a2) = halve arr
                       arr' <- forcePull (zipWith f a1 a2)
                       loop arr'

接下来,块级别归约算法的许多实例组合起来形成网格计算。以下代码使用mappConcat在多个块上分配计算。pConcat函数用于嵌套并行;它连接在每个块中计算的结果,并负责跨块的并行工作分配。(一个相关的函数是sConcat,它在一个块或一个线程中执行顺序工作。)

reduceGrid :: Scalars a => (a -> a -> a) -> Pull a -> Push Grid a
reduceGrid f arr = pConcat (map (reduceLocal f) chunks)
 where chunks = splitUp 4096 arr

这个forcePull函数阻止操作融合,因此在归约的某些阶段省略它是权衡顺序执行和并行执行的一种方式。一个optForce n函数可以被编写为仅强制执行长度小于 n 个元素的数组

optForce n arr = if len arr <= n
                 then forcePull arr
                 else return arr

这个optForce函数可以替换forcePullreduceLocal在多个块上分配计算。中。中的optForcenreduceLocal参数是要添加到的另一个可能的调整参数;另一个是splitUp4096因子(的另一个可能的调整参数;另一个是以上)。通过对强制截止值和

因子进行参数化,可以从相同的 EDSL 描述中生成一系列不同的归约代码。

Obsidian 默认情况下还提供了另一个调整参数:每个块允许的实际 GPU 线程数。此参数由 Obsidian 用户提供给代码生成器,并且可以在 CUDA 支持的每个块的线程数范围内(1-1,024)。但是,鉴于 GPU 的执行模型,只有线程束大小(32)的倍数才有意义。

允许参数在以下范围内变化的另一个可能的调整参数;另一个是

允许参数在以下范围内变化optForce因子:s∈{32,64,128,...,8192}

截止值:c∈{32,64,...,s}

• 实际线程:t∈{32,64,96,...,min(1024,s/2)}


将为自动调优创建 929 个归约代码变体。有关归约内核的基准测试结果,请参阅作者最近的技术报告。20

从 EDSL 生成 CUDA 代码到目前为止,我们已经看到了 Obsidian 的程序员视图,其中零星地涉及了一些实现细节。但是这一切是如何运作的呢?本系列的第一篇文章 (Gill) 展示了许多所需的 инфраструктура:重载、具体化和表达式数据类型。Obsidian 没什么不同;本文通篇看到的EFloatEWord32Program类型对应于表达式树。

monad 被具体化为抽象语法,表示命令式、类似 CUDA 的语言中的程序。


与 Gill 的文章相比,此处提出的一个新概念是推数组和拉数组的使用。这些延迟数组依赖于将程序片段提取为抽象语法(即,深层嵌入),但是表示推数组和拉数组的函数本身并没有捕获在抽象语法中。相反,它们就像宏一样,在 Haskell 执行期间(调用 Obsidian CUDA 发射编译器之前)进行去语法糖化(浅层嵌入)。17

CUDA 代码生成概述vecAdd函数具体化。 EDSL 函数(例如Program)应用于符号数组,导致它作为 Haskell 程序进行评估并产生其结果。Obsidian 可以具体化的函数具有推数组作为结果。因此,在应用于符号数组之后,您将获得一个推数组。推数组是一个生成Program.

的函数,将该推数组的填充函数应用于另一个符号输入会提取最终的完整ProgramMonadic 具体化。 因为for类型是一个 monad,所以应用了 monad 具体化技术(参见 Gill),结果是一个 AST(抽象语法树),描述了类似 CUDA 的命令式语言中的程序。但是,此 AST 具有显式并行

循环,而不是 CUDA 的隐式循环。force共享内存分析。 所有中间数组(由

产生)在类似 CUDA 的 AST 中都具有唯一的名称,但它们仍然必须在共享内存中布局。活跃性分析查找每个数组的首次使用和最后一次使用,并生成内存映射。AST 中的命名数组将替换为共享内存中的确切位置。CUDA 生成和线程虚拟化。 这是代码生成的最后一步。类似 CUDA 的 AST 的大多数方面都直接转换为 CUDA 源代码。显式并行循环的迭代空间可能大于实际 CUDA 线程的数量,因此需要进行进一步的转换。如果 AST 包含一个循环,"parFor(i∈[1,512]){body})",但代码生成器被指示仅使用 256 个线程,那么这个parFor


将分两个阶段发生,通过注入额外的顺序循环来实现。

结论本文重点介绍了 DSL 技术的许多潜在优势。但是,如果您要尝试使用嵌入式 DSL,也需要注意一些缺点。首先,错误消息可能会有问题,因为这些消息是用宿主语言的术语表达的。如果 Obsidian 程序员尝试在 GPU 层次结构中不正确的级别使用某些功能(例如,通过应用过多的pConcats

导致并行 for 循环嵌套过深),那么错误消息很可能会说明缺少某些 Haskell 类的实例,而不是解释为什么他们在 GPU 上尝试的操作无法完成。此外,新的 DSL 自然缺乏大型成熟语言的库和工具生态系统。force使用像 Obsidian 这样的 EDSL,它可以公开底层的架构结构,从而可以控制决定性能的细节。但是,准确选择抽象什么和公开什么仍然很重要。例如,在 Obsidian 中,程序员可以权衡顺序工作和并行工作,并使用共享内存(通过

),但是数组的共享内存布局的细节,或管理它们在该内存中的生命周期是自动化的。此外,与此同样重要的是 DSL 阻止用户做什么。在 Obsidian 中,程序员被温和但坚定地阻止编写会产生与 GPU 无法应对的 CUDA 不匹配的 DSL 代码,例如刚才提到的嵌套过深的情况。

编写 CUDA 代码并进行手动性能调整通常是一项繁琐的任务。早期做出的设计决策可能很难撤销,并且需要替换许多已编写的代码。因此,从 DSL 中的高度参数化描述生成 CUDA 代码的变体,然后凭经验选择性能最佳的变体,是很吸引人的。我们在之前的工作20中的实验表明,特定代码的最佳变体可能因 GPU 而异。


生成像 Obsidian 这样的代码生成 DSL 需要多少工作?在 Obsidian 的案例中,已经进行了多年的实验和重新设计——最终获得了博士学位!18 但不要因此而气馁。当前版本相对较小(语言实现部分 2,857 行,编译器部分 1,629 行),并且可以免费学习和复制。19 此外,用于解析、从模板生成代码(例如,http://hackage.haskell.org/package/language-c-quote)和编译器构建9的现代库使 DSL 的构建比预期的更容易。因此,下次当您发现自己编写重复的底层代码时,为什么不考虑使用嵌入式代码生成 DSL 呢?

推送数组是由 Koen Claessen 发明的。我们感谢 Andy Gill 对本文提供的宝贵反馈。Feldspar 和 Obsidian 的工作得到了瑞典战略研究基金会 RAW FP(资源感知函数式编程)项目以及国家科学基金会奖项 1337242 的支持。


参考文献

1. Axelsson, E., Claessen, K., Sheeran, M., Svenningsson, J., Engdal, D., Persson, A. 2011. Feldspar 的设计与实现:一种用于数字信号处理的嵌入式语言。载于函数式语言的实现与应用。Springer Verlag。

2. Catanzaro, B., Garland, M., Keutzer, K. 2011. Copperhead:编译一种嵌入式数据并行语言。载于第16届 并行编程原理与实践研讨会论文集:47-56。

3. Chafi, H., Sujeeth, A. K., Brown, K. J., Lee, H. J., Atreya, A. R., Olukotun, K. 2011. 异构并行性的领域特定方法。载于 SIGPLAN Notices 46: 35-46。

4. Chakravarty, M. M. T., Keller, G., Lee, S., McDonell, T. L., Grover, V. 2011. 使用多核 GPU 加速 Haskell 数组代码。载于第6届多核编程声明性方面研讨会论文集

5. Chambers, C., Raniwala, A., Perry, F., Adams, S., Henry, R. R., Bradshaw, R., Weizenbaum, N. 2010. FlumeJava:简单、高效的数据并行流水线。载于 SIGPLAN Notices 45: 363-375。

6. Chhugani, J., Nguyen, A. D., Lee, V. W., Macy, W., Hagog, M., Chen, Y.-K., Baransi, A., Kumar, S., Dubey, P. 2008. 在多核 SIMD CPU 架构上高效实现排序。载于VLDB Endowment 论文集 1(2):1313-1324。

7. Claessen, K., Sheeran, M., Svensson, B. J. 2012. 嵌入式 GPU 内核编程语言中的表达性数组构造。载于第7届多核编程声明性方面与应用研讨会论文集

8. Filipovič, J., Madzin, M., Fousek, J., Matyska, L. 2013. 通过内核融合优化 CUDA 代码——在 BLAS 上的应用。arXiv 预印本 arXiv:1305.1183。

9. Keep, A. W., Dybvig, R. K. 2013. 用于商业编译器开发的 nanopass 框架。载于第18届 SIGPLAN 函数式编程国际会议论文集:343-350。

10. Keller, G., Chakravarty, M. M. T., Leshchinskiy, R., Peyton Jones, S., Lippmeier, B. 2010. Haskell 中的规则、形状多态、并行数组。载于第15届 SIGPLAN 函数式编程国际会议论文集

11. Newburn, C. J., So, B., Liu, Z., McCool, M., Ghuloum, A., Du Toit, S., Wang, Z. G., Du, Z. H., Chen, Y., Wu, G., Guo, P., Liu, Z., Zhang, D. 2011. Intel 的数组构建模块:可重定向的动态编译器和嵌入式语言。代码生成和优化国际研讨会。

12. NVIDIA. 2013. CUDA C 编程指南;https://docs.nvda.net.cn/cuda/cuda-c-programming-guide/index.html

13. NVIDIA. 2012. Nvidia 的下一代 CUDA 计算架构:Kepler GK110;http://www.nvidia.com/content/PDF/kepler/NVIDIA-Kepler-GK110-Architecture-Whitepaper.pdf

14. Püschel, M., Moura, J. M. F., Johnson, J. R., Padua, D., Veloso, M. M., Singer, B. W., Xiong, J., Franchetti, F., Gacic, A., Voronenko, Y., et al. 2005. Spiral:用于 DSP 变换的代码生成。IEEE 论文集 93(2): 232-275。

15. Ragan-Kelley, J., Barnes, C., Adams, A., Paris, S., Durand, F., Amarasinghe, S. 2013. Halide:一种用于优化图像处理流水线中并行性、局部性和重计算的语言和编译器。载于第34届 SIGPLAN 编程语言设计与实现会议论文集:519-530。

16. Scholz, S.-B. 2003. 单赋值 C:在函数式环境中高效支持高级数组操作。函数式编程杂志 13(6): 1005-1059。

17. Svenningsson, J., Axelsson, E. 2013. 结合 EDSL 的深层和浅层嵌入。载于函数式编程趋势,由 H.-W. Loidl 和 R. Pea 编辑。计算机科学讲义 7829: 21-36. Berlin/Heidelberg: Springer。

18. Svensson, B. J. 2013. 用于数据并行编程的嵌入式语言。博士论文。查尔姆斯理工大学计算机科学与工程系。

19. Svensson, B. J. 2014. Obsidian GitHub 仓库;https://github.com/svenssonjoel/Obsidian

20. Svensson, B. J., Sheeran, M., Newton, R. R. 2014. 一种用于 GPU 上嵌套数据并行设计空间探索的语言。技术报告 712。印第安纳大学;http://www.cs.indiana.edu/cgi-bin/techreports/TRNNN.cgi?trnum=TR712

21. Tang, Y., Chowdhury, R. A., Kuszmaul, B. C., Luk, C.-K., Leiserson, C. E. 2011. pochoir 模板编译器。载于第23届 并行算法与体系结构研讨会论文集:117-128。

22. Thies, W., Karczmarek, M., Amarasinghe, S. 2002. StreamIt:一种用于流式应用的语言。载于编译器构造:179-196. Springer。


喜欢还是讨厌?请告诉我们

[email protected]


Bo Joel Svensson 于 2013 年在瑞典哥德堡查尔姆斯理工大学获得计算机科学博士学位。他的导师是 Mary Sheeran、Koen Claessen 和 Josef Svenningsson。他目前是印第安纳大学的博士后,与 Ryan R. Newton 合作开展一个与 EDSLs 和并行性相关的项目。

Mary Sheeran 是瑞典哥德堡查尔姆斯理工大学软件技术部门的教授,也是函数式编程小组成员。自 1980 年代初期以来,她一直从事领域特定语言的研究,当时她研究了一种函数式硬件描述语言。她仍然对函数式编程如何帮助底层编程或硬件设计感兴趣。她拥有电气工程背景和牛津大学计算学博士学位。

Ryan R. Newton 于 2009 年在麻省理工学院获得计算机科学博士学位。之后,他在英特尔开发者产品部门从事并行编程工具的研究。2011 年,他加入印第安纳大学,在那里他的研究重点是基于语言的方法,以应对未来架构带来的编程挑战。为此,他和他的学生发明了一种新的用于确定性并行性的并发数据抽象(LVars),并且正在研究用于数组语言和分布式流处理的领域特定编译器。

© 2014 1542-7730/14/0400 $10.00

acmqueue

最初发表于 Queue 杂志第 12 卷第 4 期——
数字图书馆中评论本文





更多相关文章

Matt Godbolt - C++ 编译器中的优化
在向编译器提供更多信息时需要权衡:这可能会使编译速度变慢。链接时优化等技术可以为您提供两全其美的方案。编译器中的优化在不断改进,即将到来的间接调用和虚函数分派的改进可能很快会带来更快的多态性。


Ulan Degenbaev, Michael Lippautz, Hannes Payer - 作为合资企业的垃圾回收
跨组件追踪是解决跨组件边界的引用循环问题的一种方法。一旦组件可以形成在 API 边界上具有重要所有权的任意对象图,就会出现此问题。CCT 的增量版本已在 V8 和 Blink 中实现,从而能够以安全的方式有效且高效地回收内存。


David Chisnall - C 不是一种低级语言
鉴于最近的 Meltdown 和 Spectre 漏洞,值得花一些时间研究根本原因。这两个漏洞都涉及处理器推测性地执行超出某种访问检查的指令,并允许攻击者通过侧信道观察结果。导致这些漏洞以及其他几个漏洞的功能被添加进来,是为了让 C 程序员继续相信他们正在使用低级语言进行编程,尽管这种情况已经几十年没有发生了。


Tobias Lauinger, Abdelberi Chaabane, Christo Wilson - 你不应该依赖我
大多数网站都使用 JavaScript 库,其中许多库已知存在漏洞。了解问题的范围以及包含库的许多意外方式,只是改善情况的第一步。这里的目标是,本文中包含的信息将有助于为社区提供更好的工具、开发实践和教育工作。





© All Rights Reserved.

© . All rights reserved.