C++-CnC

CnC

入门

CnC概述

CnC是一种新型编程模型,是为创建并行应用程序、而非为明确表达并行性而设计的。
在CnC中,程序员只需要声明指定计算单元之间的依赖关系,但并不给出任何方式来指示如何满足这些依赖关系,CnC就专门用于解决潜在并行计算单元和数据之间的协调问题。本质上,程序员声明了两个或多个计算单元之间的某些依赖关系。指定依赖项比表达并行性更简单,因为它只使应用程序语义显式,并且不依赖于平台或任何特定的并行化技术。
此外,CnC还体现了更多并行性方面的潜力,因为它没有将特定的并行性绑定到算法/程序。在CnC运行时会做搞清楚如何并行执行的事情的努力; 它将尝试最大化并行性,仅受程序员定义的排序约束的限制。
在任何程序中语义上存在的仅有的2个排序约束来自以下2个关系:生产者/消费者、控制者/被控制者。很明显,生产者需要先执行,消费者才能运行。类似地,如果一个计算单元决定是否需要执行另一计算,则决策者(例如控制器)需要在受控计算之前进行。这两种关系(生产者/消费者和控制器/被控制者)是确定语义上正确的并行或顺序执行顺序所需的唯一关系。任何程序员都知道此信息,即使在编写顺序程序时也是如此。它只会丢失,因为传统的编程语言无法明确表达它。这正是CnC程序指定的内容(当然是在计算功能之上)。

CnC设计关键

使用CnC的挑战当然在于将其概念应用于给定的问题(应用程序)。幸运的是,一旦完成,剩下的任务就很简单了,以后对设计的更改也相对简单。为您的应用程序获得完整的CnC设计的思考过程可以发生在几个小阶段:

  1. 定义应用程序的数据和计算实体(步骤、项目和标签集合)
  2. 定义如何区分这些实体的实例(标识符/标签是什么样的?)
  3. 定义实体之间的关系(生产者/消费者和控制者/控制者)

本次调查报告将通过创建一个计算斐波那契数的程序来解释CnC与C++ API的使用。由于斐波那契CnC的性质,它不一定能提供最自然的表现力。然而,Fibonacci具有适当的复杂性和特性来演示CnC和英特尔的C++ API,而不会迷失在非CnC相关问题的细节中。
创建第一个程序后,我们将介绍调试界面和更高级的功能,例如提供强大调谐旋钮的调谐界面。

实例

这里采用斐波那契数列问题作为例子,并不是因为采用CnC的实现方法是该问题的最优解,而是斐波那契问题本身足够简单,可以清晰地显示CnC的一些显著优势。
给定n的斐波那
契数列可以用递归的方式表示,有
f i b ( n ) = f i b ( n − 1 ) + f i b ( n − 2 ) fib(n)=fib(n-1)+fib(n-2) fib(n)=fib(n1)+fib(n2)

由于计算结果值可能会变得很大,所以此处先定义数据类型,给数据类型可以根据实际需要进行调整,如下

typedef unsigned long long fib_type;

CnC规范

计算单元 computation units

斐波那契数列的算法非常简单,只有将前两个数值相加的一步计算,该步骤被称为fib_step。在CnC中,一类函数的所有执行实例都保存在一个集合中,即CnC::step_collection。我们需要为类型为fib_step的计算步骤申明一个step-collection实例,并命名为m_steps,如下

CnC::step_collection<fib_step> m_steps;

由于不需要其他函数再来计算斐波那契数列,一个step-collection就足够了。当然,更复杂的程序可以使用多个step-collection。

数据项目 data items

我们唯一关心的数据就是斐波那契数列中计算出的结果数值。在CnC中,从计算中读取数值的唯一途径就是通过item-collection获取。所以,显然需要将计算结果存储在一个item-collection中。此外,我们还需要另一次的计算结果来计算下一次新的结果数值。幸运的是,斐波那契数列是递归数列,输入数据和输出数据种类相同,这里指的不仅是数据类型相同,并且两者的含义也相同。因此,可以对中间结果和最终结果使用同一个item-collection,如下

CnC::item_collection<int, fib_type> m_fibs;

CnC的C++ API提供了模板CnC::item_collection来定义item-collection。其中,第二个参数是存储数据的类型,对于斐波那契数列,我们申明了其数据类型为fib_type,因此fib_type就是第二个参数;第一个参数是用于标识每个数据实例的标签类型,就像传统的key/value数值对一样,我们可以通过tag来访问对应的item。我们可以简单的整数来作为斐波那契数列中各数值的标签,所以在语义上,上述的item-collection将一个整数key映射到相应的斐波那契value,同时fib_step也将在程序执行期间填充满整个数据结构。
通常情况下,只要C++的类或类型提供的是复制构造函数,就可以将它们作为item或tag,事实上大多数都是如此。同时,tag的类型必须分别由哈希函数支持,默认情况下一般用cnc_tag_hash_compare

步骤 steps

显然,我们需要根据给定的实际值来计算斐波那契数列。CnC中的计算单元被称为step,如此的一个步骤是个带有执行方法的类,接收2个参数:控制标签和第二个参数,通常是上下文但也可以是其他参数,如下

struct fib_step
{
    // declaration of execute method goes here
    int execute(const int &tag, fib_context &c) const;
};

值得注意的是,此处的执行方法的关键字必须是const,一定程度上可以提高整个程序的可靠性和安全性。
从step的角度来看,控制标签可以区分同一步骤中的执行实例。例如,控制标签可以告诉之前的fib_step当前执行的是哪个斐波那契数,并且这不会与输入数据混淆,因为输入数据是由item-collection处理的。
通常建议用const来传递标签,尤其是在程序使用不同标准数据类型的情况下。

控制标签 control tags

在CnC中,我们从不使用显示的步骤。如果需要给定tag来执行step,就将该标签放入tag-collection中,该集合会保证某个步骤最终会被执行完毕。因此,标签会告诉CnC运行时需要执行的步骤实例,但不会说明该实例何时运行。所以,我们定义一个tag-collection来控制上述的step-collection,同时命名为m_tags,如下

CnC::tag_collection<int> m_tags;

CnC的C++ API提供了类似的模板CnC::tag_collection来定义tag-collection。同时,tag可以是任何类型的,只要它能提供复制构造函数即可。

上下文 content

在实际编写步骤代码之前,我们需要用到上文提到的上下文(context),在CnC中通常被称为图形(graph)。它通过将不同的集合定义为图形成员来结合起来。在图形内部,上下文负责执行各个步骤的时机,并用于控制图形的评估,例如,是否要等待当前步骤完成。因此,CnC建议将tag-collection和item-collection定义为上下文的成员。
每个上下文都是必须派生自一个基类,该基类是一个模板,接收的模板参数是新定义的派生类,如下

struct fib_context : public CnC::context<fib_context>
// derive from CnC::context
{
    // the step collection for the instances of the compute-kernel
    CnC::step_collection<fib_step> m_steps;
    // item collection holding the fib number(s)
    CnC::item_collection<int, fib_type> m_fibs;
    // tag collection to control steps
    CnC::tag_collection<int> m_tags;
    // constructor
    fib_context();
};

至此为止,我们已经有了关于集合和上下文的所有定义。现在,我们需要明确一些机制,例如,不同集合之间的关系。如上文所述,生产者-消费者关系是通过调用各自的生产-消费方法来进行声明的。然后,我们希望每m_tags中的每个tag都会执行一个m_steps中的step,这可以通过简单地在m_tags上调用prescribes来实现。我们在上下文构造函数中声明上述所有关系,如下

fib_context::fib_context()
    : CnC::context<fib_context>(),
    // pass context to collection constructors
    m_steps(*this),
    m_fibs(*this),
    m_tags(*this)
{
    // prescribe compute steps with this (context) as argument
    m_tags.prescribes(m_steps, *this);
    // step consumes m_fibs
    m_steps.consumes(m_fibs);
    // step also produces m_fibs
    m_steps.produces(m_fibs);
}

编写步骤

现在我们已经完成CnC图形的设置,准备从功能上来定义步骤。步骤的第二个参数就是context,通过它可以访问tag-collection和item-collection。为了计算给定的结果,我们需要它的前两个结果数值,这可以从item-collection中读取;发布或生成结果则是相反的操作,将结果数值存入item collection。整个步骤的代码,如下

int fib_step::execute(const int &tag, fib_context &ctxt) const
{
    switch(tag) {
        case 0: ctxt.m_fibs.put(tag, 0); break;
        case 1: ctxt.m_fibs.put(tag, 1); break;
        default:
            // get previous 2 results
            fib_type f_1; ctxt.m_fibs.get(tag - 1, f_1);
            fib_type f_2; ctxt.m_fibs.get(tag - 2, f_2);
            // put our result
            ctxt.m_fibs.put(tag, f_1 + f_2);
    }
    return CnC::CNC_Success;
}

值得注意的是,step并不关心输入数据来自哪里,也不关心结果输出到哪里,它只负责请求或获取需要的item实例,再传出它计算出的新item实例。CnC在运行时会采取一切措施来协调程序中存在的生产者-消费者关系,所以实际使用中用户并不需要替CnC考虑这些问题。

编写主函数

现在,来编写主函数。首先,我们把上下文实例化,如下

fib_context ctxt;

再开启对斐波那契数列的评估,如下

for(int i = 0; i <= n; ++i) ctxt.m_tags.put(i);

这时,我们发现 f i b ( n − 1 ) fib(n-1) fib(n1) 在实行相应的step之前会不可用。因此,只定义所需要的步骤实例是远远不够的,这也就是我们为什么需要将tag数量扩充到n。
然后,等待评估完成,如下

ctxt.wait();

完整的主程序包括了从命令行读取所需输入值一直到将结果打印输出在终端上,如下

int main(int argc, char* argv[])
{
    int n = 42;
    // eval command line args
    if(argc < 2) {
        std::cout << "usage: " << argv[0] << " n\nUsing default value " << n << std::endl;
    } else {
        n = atol(argv[1]);
    }
    // create context
    fib_context ctxt;
    // put tags to initiate evaluation
    for(int i = 0; i <= n; ++i) ctxt.m_tags.put(i);
    // wait for completion
    ctxt.wait();
    // get result
    fib_type res2;
    ctxt.m_fibs.get(n, res2);
    // print result
    std::cout << "fib (" << n << "): " << res2 << std::endl;
    return 0;
}

功能调试

追踪

当用户在CnC引擎下运行项目时,可能会对实际发生了什么产生兴趣。CnC C++ API提供了一个接口可以方便地对正在运行的具体项目进行调试输出,用户可以调整需要或不需要查看的部分。
首先,需要包含如下头文件

#include <cnc/cnc_debug.h>

该头文件声明了调试接口。通过使用相应的集合调用CnC::debug::trace,可以为step-collection、tag-collection、item-collection检索调试输出。如果要追踪斐波那契的step-collection和item-collection,则可以在context构造函数中或在创建context后进行相应的调用,如下

// enable debug output for steps
CnC::debug::trace(ctxt.m_steps);
// also enable debug output for our items
CnC::debug::trace(ctxt.m_fibs);

运行程序时,用户可以看到每个step的调用和每个item的放置或获取。这些追踪项目包括追踪成功或追踪不成功的注释。
如果程序中使用多个step-collection、tag-collection、item-collection,就会很难确定默认追踪的条目属于哪个集合。所以,要使追踪更有意义,我们可以为每个集合指定名称。因此,这些集合的构造函数可以选择性地接收字符串参数,如下

fib_context()
    : CnC::context<fib_context>(),
    // Initialize each step collection
    m_steps(*this, "fib_step"),
    // Initialize each tag collection
    m_fibs(*this, "fibs"),
    // Initialize each item collection
    m_tags(*this, "tags")

调度统计

CnC还有一个有趣的功能,是可以提供有关内部程序调度的统计信息。根据依赖项是否可用的信息,某个step可能已经提前执行,并且可以在已丢失的item可用时重复执行。当context被销毁时,即程序终止时,可以打印相关信息。若要启用以上功能,我们可以直接调用
CnC::debug::collect_scheduler_statistics,如下

CnC::debug::collect_scheduler_statistics(ctxt);

在实际应用中,我们可以不将程序调度统计放入context构造函数中,但这样做可以同时启用追踪和调度统计,也是个不错的选择。

优化器

CnC的概念允许独立于实际程序核心的优化方案。这种优化不需要详细了解程序代码,只需要再集合定义中添加一些声明,优化界面就可以提供相应的优化提示。并且,该优化界面兼具模块化、灵活、易于使用的特点。

数据依赖预声明

CnC的通用性较好,但这在运行时就需要付出一些代价。为了加快对特定应用程序的评估速度,CnC C++ API提供的功能实际上会影响执行时的性能。最重要的是,API允许为每个集合指定一个优化器(tuner)。通过优化器,我们可以为运行时的CnC提供各种优化提示,tuner type是集合类可以选择的模板参数。具体如何将优化器类型fib_tuner分配给step-collection,如下

CnC::step_collection<fib_step, fib_tuner> m_steps;

为定义优化器,我们应该从CnC运行时提供的默认实现中派生所需的tuner class。否则,即使只打算使用部分的优化界面,也需要将整个界面都显示出来。适合step-collection的优化器类是CNC::step_tuner<>,如下

struct fib_tuner : public CnC::step_tuner<>

<>是源于C++中处理默认模板参数的方式。只有在更高级的范围(调整范围)使用时才需要这类默认参数。
除了可以在分布式内存上运行CnC应用程序,和step优化器关联最紧密的功能就是对item依赖的预声明。放置tag后,正常CnC程序执行时会创建一个step实例,并将其移交给底层调度程序,底层程序会在最后启动该步骤。只有在实际执行程序期间,我们可以观察到item不可用,比如数据获取失败,并重新安排当前step。如果优化器事先对item进行声明,step实例则不会在相关item可用前被部署好。为对依赖项进行预声明,我们需要提供模板方法depends,它可以接收和step::execute相同的参数和一个额外参数,如下

struct fib_tuner : public CnC::step_tuner<>
{
    template<class dependency_consumer>
    void depends(const int &tag, fib_context &c, dependency_consumer &dC) const;
};

通过在提供的模板对象上调用depends,可以直接声明依赖项,如下

template<class dependency_consumer>
void fib_tuner::depends(const int &tag, fib_context &c, dependency_consumer &dC) const
{
    // we have item-dependencies only if tag > 1
    if(tag > 1) {
        dC.depends(c.m_fibs, tag - 1);
        dC.depends(c.m_fibs, tag - 2);
    }
}

通过预先调度一个step可以实现类似的效果,同一线程上执行一个step,该线程放置给定的tag直到访问到首个不可用的item。显然,如果所有item都可用,这种机制就不会使程序并行执行,因为整个step都能被完整执行。不过,这种机制更为简单,并且可以提高性能。
预调度可以通过一个优化器来完成,该优化器的返回值为true时就会提供pre-schedule方法,如下

struct fib_tuner : public CnC::step_tuner<>
{
    bool preschedule() const { return true; }
};

有趣的是,我们可以将非安全获取操作和context::flush_gets()预调度结合,可能会产生一些有意想不到的效果。例如,可以在 预调度阶段禁止除了调用context::flush_gets()之外的step执行,这样反而会增加并行度。

项目集合优化

如果没有附加信息,程序无法决定何时不在对item进行发布。然而,如果没有此类信息,我们就无法从内存中删除数据项。CnC从不声明某事何时发生,所以通常也不会让用户指明哪个item是在最后获取的。
由于将数据项保存在内存中会快速且不必要地耗尽可用内存空间,因此需要一种机制来防止此类事件发生。放置item时,CnC C++ API允许用户声明发出获取的次数get_count。当达到声明的次数时,运行的程序将从内存中删除当前item,来释放内存以供其他用途。
在斐波那契数列实例中,每个中间项 ‘x’ 将被恰好访问两次:一次被 f i b ( x + 1 ) fib(x+1) fib(x+1) 访问、一次被 f i b ( x + 2 ) fib(x+2) fib(x+2) 访问。因此,每个斐波那契item的get_count是2。
get_count是item的一个属性,因此其必要的功能在item优化器中。通过提供一个实际放置item的const,get_count是可以直接声明的,如下

int item_tuner::get_count(const int &tag) const
{
    return tag > 0 ? 2 : 1;
}

和tag-collection、step-collection一样,优化器需要在item-collection被定义为可用,如下

CnC::item_collection<int, fib_type, item_tuner> m_fibs;

一旦被访问两次,该item就会被释放。get_count在很大程度上能控制内存被占用的情况,因此可以显著提高程序的整体性能。
需要注意的是,当前版本的CnC不会减少环境给出的get_count,比如,在执行step以外的时间get_count是不变的。为避免get_count功能发出警告,被环境消耗或获取的item的返回值设为CnC::NO_GETCOUNT

标签/步骤缓存

由于step实例是无状态执行的,多次执行相同的函数并不会改变结果,并且只会增加不必要的运行成本。在斐波那契数列中,大多数控制步骤的标签被多次重复放置。默认情况下,CnC运行时放置tag的次数和执行其相应step的次数是相同的。实际运行时可以通过一个简单的优化提示,程序将自动省略重复的步骤。然而,这个功能在默认情况下是不开启的,因为重复的标签并不常见,而且这会增加自动缓存的成本。
缓存是tag-cllection中的问题,因此通过将优化器分配给tag-cllection来提供相应的优化功能。我们可以通过保留tag来启用缓存,CnC也为tag优化器提供了相应的基类便于调用,如下

CnC::tag_collection<int, CnC::preserve_tuner<int>> m_tags;

以上就是让运行时的程序对执行step进行缓存的全部内容。在斐波那契数列中,这个功能产生了显著的影响,因为由于该算法本身是源于冗余计算的,所以它省略了大部分的计算量,从而提高程序的整体运行速度。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值