C++ 处理特定的 impl - #ifdef vs 私有(private)继承 vs 标签调度

标签 c++ oop

我有一些类实现了一些我有的计算
针对不同的 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

优点:
  • 拆分非常简单易行
  • 我的类(class)对象没有“开销”(不知道怎么说更好)
    我的意思是没有额外的继承,没有添加成员变量等。
  • #ifdef 干净多了-到处都是

  • 缺点:
  • 我有三个额外的文件需要维护;我可以把标量
    虽然在 Algo.cpp 文件中实现,但最终只有两个,但
    夹杂物部分看起来会更脏一点
  • 它们本身不是可编译的单元,必须从
    项目结构
  • 如果我还没有具体的优化实现,让我们说
    SSE 我必须从普通(标量)C++ 实现文件中复制一些代码
  • 如果 nedded,我不能回退到普通的 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 干净多了-到处都是
  • 我的类(class)仍然没有“开销” - EBCO 应该开始
  • 至少与上述相比,类的语义要清晰得多
    private inheritance == is implemented in terms of
  • 不同的文件是可编译的,可以包含在项目中
    并通过构建系统选择

  • 缺点:
  • 我有三个额外的文件需要维护
  • 如果我还没有具体的优化实现,让我们说
    SSE 我必须从普通(标量)C++ 实现文件中复制一些代码
  • 如果 nedded
  • 我不能回退到普通的 C++ 实现

    3.基本上是方法2,但在AlgoImpl中带有虚函数类(class)。那
    如果需要,将允许我克服纯 C++ 代码的重复实现
    通过在基类中提供空实现并在派生类中覆盖
    虽然我在实际实现优化时必须禁用该行为
    版本。此外,虚函数会给我的类的对象带来一些“开销”。

    4.一种通过 enable_if<> 进行标签调度的形式

    优点:
  • 拆分非常简单易行
  • 比 #ifdef 更干净
  • 我的类(class)仍然没有“开销”
  • 将消除不同实现对不同文件的需求

  • 缺点:
  • 模板会有点“神秘”,似乎带来了不必要的
    开销(至少对于某些情况下的某些人而言)
  • 如果我还没有具体的优化实现,让我们说
    SSE 我将不得不从普通(标量)C++ 实现中复制一些代码
  • 如果需要,我不能回退到普通的 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);
    }
    

    在这种情况下,您最终会获得 MatrixVector类型知道 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/

    相关文章:

    oop - 领域模型中的特化层次结构

    javascript - 在 javascript 中记录对象时如何强制 toString

    c++ - 将 char * 转换为 string *

    c++ - operator-> "chained"是指针吗?

    c# - .NET 互操作是来回复制数组数据,还是固定数组?

    javascript - 自行车构造器不断添加齿轮而不是输出指定数量

    Java - 如何判断对象的类?

    c++ - Metalog 所有日志都打印有[kernel]

    c++ - 模板参数包属性

    java - 在延迟初始化和一般情况下,覆盖可以有效替代 if 语句吗?