我有一些类实现了一些我有的计算
针对不同的 SIMD 实现进行优化,例如阿尔托和
上海证券交易所。我不想用 #ifdef ... #endif
污染代码块
对于我必须优化的每种方法,所以我尝试了其他几种
接近,但不幸的是,我对它的转变方式不太满意
出于我会尽力澄清的原因。所以我正在寻找一些建议
关于如何改进我已经完成的工作。
1.粗略的不同实现文件包括
我有相同的头文件,描述了不同的类接口(interface)
纯 C++、Altivec 和 SSE 的“伪”实现文件仅用于
相关方法:
// Algo.h
#ifndef ALGO_H_INCLUDED_
#define ALGO_H_INCLUDED_
class Algo
{
public:
Algo();
~Algo();
void process();
protected:
void computeSome();
void computeMore();
};
#endif
// Algo.cpp
#include "Algo.h"
Algo::Algo() { }
Algo::~Algo() { }
void Algo::process()
{
computeSome();
computeMore();
}
#if defined(ALTIVEC)
#include "Algo_Altivec.cpp"
#elif defined(SSE)
#include "Algo_SSE.cpp"
#else
#include "Algo_Scalar.cpp"
#endif
// Algo_Altivec.cpp
void Algo::computeSome()
{
}
void Algo::computeMore()
{
}
... same for the other implementation files
优点:
我的意思是没有额外的继承,没有添加成员变量等。
#ifdef
干净多了-到处都是缺点:
虽然在 Algo.cpp 文件中实现,但最终只有两个,但
夹杂物部分看起来会更脏一点
项目结构
SSE 我必须从普通(标量)C++ 实现文件中复制一些代码
在描述的场景中做到这一点?
2.私有(private)继承的不同实现文件
// Algo.h
class Algo : private AlgoImpl
{
... as before
}
// AlgoImpl.h
#ifndef ALGOIMPL_H_INCLUDED_
#define ALGOIMPL_H_INCLUDED_
class AlgoImpl
{
protected:
AlgoImpl();
~AlgoImpl();
void computeSomeImpl();
void computeMoreImpl();
};
#endif
// Algo.cpp
...
void Algo::computeSome()
{
computeSomeImpl();
}
void Algo::computeMore()
{
computeMoreImpl();
}
// Algo_SSE.cpp
AlgoImpl::AlgoImpl()
{
}
AlgoImpl::~AlgoImpl()
{
}
void AlgoImpl::computeSomeImpl()
{
}
void AlgoImpl::computeMoreImpl()
{
}
优点:
#ifdef
干净多了-到处都是即
private inheritance == is implemented in terms of
并通过构建系统选择
缺点:
SSE 我必须从普通(标量)C++ 实现文件中复制一些代码
3.基本上是方法2,但在AlgoImpl中带有虚函数类(class)。那
如果需要,将允许我克服纯 C++ 代码的重复实现
通过在基类中提供空实现并在派生类中覆盖
虽然我在实际实现优化时必须禁用该行为
版本。此外,虚函数会给我的类的对象带来一些“开销”。
4.一种通过 enable_if<> 进行标签调度的形式
优点:
缺点:
开销(至少对于某些情况下的某些人而言)
SSE 我将不得不从普通(标量)C++ 实现中复制一些代码
对于任何变体,我还无法弄清楚如何正确和
干净地回退到普通的 C++ 实现。
此外,我不想过度设计事物,在这方面,第一个变体
即使考虑到缺点,似乎也是最“KISS”的。
最佳答案
根据评论中的要求,以下是我所做的总结:
设置 policy_list
辅助模板实用程序
这会维护一个策略列表,并在调用第一个合适的实现之前给它们一个“运行时检查”调用
#include <cassert>
template <typename P, typename N=void>
struct policy_list {
static void apply() {
if (P::runtime_check()) {
P::impl();
}
else {
N::apply();
}
}
};
template <typename P>
struct policy_list<P,void> {
static void apply() {
assert(P::runtime_check());
P::impl();
}
};
制定具体政策
这些策略实现了运行时测试和相关算法的实际实现。对于我的实际问题, impl 使用了另一个模板参数,该参数指定了他们正在实现的内容,尽管这里的示例假设只有一件事要实现。运行时测试缓存在
static bool
中对于某些人(例如我使用的 Altivec),测试真的很慢。对于其他人(例如 OpenCL 的),测试实际上是“这个函数指针是 NULL
吗?”在尝试使用 dlsym()
设置它后.#include <iostream>
// runtime SSE detection (That's another question!)
extern bool have_sse();
struct sse_policy {
static void impl() {
std::cout << "SSE" << std::endl;
}
static bool runtime_check() {
static bool result = have_sse();
// have_sse lives in another TU and does some cpuid asm stuff
return result;
}
};
// Runtime OpenCL detection
extern bool have_opencl();
struct opencl_policy {
static void impl() {
std::cout << "OpenCL" << std::endl;
}
static bool runtime_check() {
static bool result = have_opencl();
// have_opencl lives in another TU and does some LoadLibrary or dlopen()
return result;
}
};
struct basic_policy {
static void impl() {
std::cout << "Standard C++ policy" << std::endl;
}
static bool runtime_check() { return true; } // All implementations do this
};
按架构设置
policy_list
简单示例根据
ARCH_HAS_SSE
设置两个可能列表之一预处理器宏。你可以从你的构建脚本中生成它,或者使用一系列 typedef
s,或对 policy_list
中的“漏洞”的黑客支持在某些架构上,直接跳到下一个架构,而不尝试检查支持,这可能是无效的。 GCC 为您设置了一些可能有帮助的预处理器宏,例如__SSE2__
.#ifdef ARCH_HAS_SSE
typedef policy_list<opencl_policy,
policy_list<sse_policy,
policy_list<basic_policy
> > > active_policy;
#else
typedef policy_list<opencl_policy,
policy_list<basic_policy
> > active_policy;
#endif
您也可以使用它在同一平台上编译多个变体,例如x86 上的 SSE 和无 SSE 二进制文件。
使用策略列表
相当简单,请调用
apply()
policy_list
上的静态方法.相信它会拨打 impl()
通过运行时测试的第一个策略上的方法。int main() {
active_policy::apply();
}
如果您采用我之前提到的“每个操作模板”方法,它可能更像是:
int main() {
Matrix m1, m2;
Vector v1;
active_policy::apply<matrix_mult_t>(m1, m2);
active_policy::apply<vector_mult_t>(m1, v1);
}
在这种情况下,您最终会获得
Matrix
和 Vector
类型知道 policy_list
以便他们可以决定如何/在哪里存储数据。您也可以为此使用启发式方法,例如“无论如何,小 vector/矩阵都存在于主内存中”并使 runtime_check()
或另一个功能测试特定方法对特定实例的给定实现的适当性。我还有一个自定义的容器分配器,它总是在任何启用 SSE/Altivec 的构建上生成适当对齐的内存,无论特定机器是否支持 Altivec。那样更容易,尽管它可能是
typedef
在给定的策略中,您总是假设最高优先级的策略具有最严格的分配器需求。示例
have_altivec()
:我已经包含了一个样本
have_altivec()
实现完整性,仅仅因为它是最短的,因此最适合在这里发布。 x86/x86_64 CPUID 一团糟,因为您必须支持编写内联 ASM 的编译器特定方式。 OpenCL 一团糟,因为我们也检查了一些实现限制和扩展。#if HAVE_SETJMP && !(defined(__APPLE__) && defined(__MACH__))
jmp_buf jmpbuf;
void illegal_instruction(int sig) {
// Bad in general - https://www.securecoding.cert.org/confluence/display/seccode/SIG32-C.+Do+not+call+longjmp%28%29+from+inside+a+signal+handler
// But actually Ok on this platform in this scenario
longjmp(jmpbuf, 1);
}
#endif
bool have_altivec()
{
volatile sig_atomic_t altivec = 0;
#ifdef __APPLE__
int selectors[2] = { CTL_HW, HW_VECTORUNIT };
int hasVectorUnit = 0;
size_t length = sizeof(hasVectorUnit);
int error = sysctl(selectors, 2, &hasVectorUnit, &length, NULL, 0);
if (0 == error)
altivec = (hasVectorUnit != 0);
#elif HAVE_SETJMP_H
void (*handler) (int sig);
handler = signal(SIGILL, illegal_instruction);
if (setjmp(jmpbuf) == 0) {
asm volatile ("mtspr 256, %0\n\t" "vand %%v0, %%v0, %%v0"::"r" (-1));
altivec = 1;
}
signal(SIGILL, handler);
#endif
return altivec;
}
结论
基本上,对于永远无法支持实现的平台(编译器不为它们生成任何代码),您不会支付任何惩罚,并且只会受到很小的惩罚(如果您的编译器在优化方面表现不佳,则可能只是 CPU 测试/jmp 对非常可预测的)对于可以支持某些东西但不支持的平台。您无需为运行首选实现的平台支付额外费用。运行时测试的细节因所讨论的技术而异。
关于C++ 处理特定的 impl - #ifdef vs 私有(private)继承 vs 标签调度,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7548975/