组织多个实现(对于 SIMD)
Organizing multiple implementations (for SIMD)
这确实是一个 open-ended/subjective 问题,但我正在寻找关于如何“组织”相同功能的多个替代实现的不同想法。
我有一组函数,每个函数都有特定于平台的实现。具体来说,它们各自针对特定的 SIMD 类型具有不同的实现:NEON(64 位)、NEON(128 位)、SSE3、AVX2 等(以及一种非 SIMD 实现)。
所有函数都有非 SIMD 实现。并非所有函数都专用于每种 SIMD 类型。
目前,我有一个整体文件,它使用一堆#ifdef 来实现特定的 SIMD 专业化。当我们只将一些函数专门化为一种或两种 SIMD 类型时,它就起作用了。现在,它变得笨重了。
实际上,我需要一些功能类似于 virtual/override 的东西。非 SIMD 实现在基础 class 中实现,SIMD 特化(如果有)将覆盖它们。但我不想要实际的运行时多态性。此代码对性能至关重要,许多函数可以(并且应该)内联。
沿着这些路线的东西将完成我需要的东西(这仍然是一团乱麻的#ifdefs)。
// functions.h
void function1();
void function2();
#ifdef __ARM_NEON
#include "functions_neon64.h"
#elif __SSE3__
#include "functions_sse3.h"
#endif
#include "functions_unoptimized.h"
// functions_neon64.h
#ifndef FUNCTION1_IMPL
#define FUNCTION1_IMPL
void function1() {
// NEON64 implementation
}
#endif
// functions_sse3.h
#ifndef FUNCTION2_IMPL
#define FUNCTION2_IMPL
void function2() {
// SSE3 implementation
}
#endif
// functions_unoptimized.h
#ifndef FUNCTION1_IMPL
#define FUNCTION1_IMPL
void function1() {
// Non-SIMD implementation
}
#endif
#ifndef FUNCTION2_IMPL
#define FUNCTION2_IMPL
void function2() {
// Non-SIMD implementation
}
#endif
有人有更好的主意吗?
以下只是我在思考时想到的一些想法 - 可能还有我不知道的更好的解决方案。
1。 Tag-Dispatch
使用 Tag-Dispatch 您可以定义编译器应考虑的函数顺序,例如在这种情况下是
AVX2 -> SSE3 -> Neon128 -> Neon64 -> None
将使用此链中存在的第一个实现:godbolt example
/**********************************
** functions.h *******************
*********************************/
struct SIMD_None_t {};
struct SIMD_Neon64_t : SIMD_None_t {};
struct SIMD_Neon128_t : SIMD_Neon64_t {};
struct SIMD_SSE3_t : SIMD_Neon128_t {};
struct SIMD_AVX2_t : SIMD_SSE3_t {};
struct SIMD_Any_t : SIMD_AVX2_t {};
#include "functions_unoptimized.h"
#ifdef __ARM_NEON
#include "functions_neon64.h"
#endif
#ifdef __SSE3__
#include "functions_see3.h"
#endif
// etc...
#include "functions_stubs.h"
/**********************************
** functions_unoptimized.h *******
*********************************/
inline int add(int a, int b, SIMD_None_t) {
std::cout << "NONE" << std::endl;
return a + b;
}
/**********************************
** functions_neon64.h ************
*********************************/
inline int add(int a, int b, SIMD_Neon64_t) {
std::cout << "NEON!" << std::endl;
return a + b;
}
/**********************************
** functions_neon128.h ***********
*********************************/
inline int add(int a, int b, SIMD_Neon128_t) {
std::cout << "NEON128!" << std::endl;
return a + b;
}
/**********************************
** functions_stubs.h *************
*********************************/
inline int add(int a, int b) {
return add(a, b, SIMD_Any_t{});
}
/**********************************
** main.cpp **********************
*********************************/
#include "functions.h"
int main() {
add(1, 2);
}
这将输出 NEON128!
,因为这是本例中的最佳匹配。
优点:
- 实施中不需要
#ifdef
个 header 个文件
- 调用者不需要修改
缺点:
- 您需要为每个实现添加一个额外的参数
- 需要 dispatch-function 来提供额外的参数
(理论上,您可以通过在调用该函数的任何地方添加 , SIMD_Any_t{}
来摆脱该函数,但这需要大量工作)
2。将函数放入 classes 并使用名称查找来选择正确的函数
例如:
struct None { inline static int add(int a, int b) { return a + b; } };
struct Neon64 : None { inline static int add(int a, int b) { return a + b; } };
struct Neon128 : Neon64 {};
struct SIMD : Neon128 {};
// Usage:
int r = SIMD::add(1, 2);
因为 child classes 可以隐藏其 base-classes 的成员,这不是歧义。 (总是会调用实现给定方法的 most-derived class,因此您可以订购您的实现)
对于您的示例,它可能如下所示:godbolt example
#include <iostream>
/**********************************
** functions.h *******************
*********************************/
#include "functions_unoptimized.h"
#ifdef __ARM_NEON
#include "functions_neon64.h"
#else
struct SIMD_Neon64 : SIMD_None {};
#endif
#ifdef __ARM_NEON_128
#include "functions_neon128.h"
#else
struct SIMD_Neon128 : SIMD_Neon64 {};
#endif
// etc...
struct SIMD : SIMD_Neon128 {};
/**********************************
** functions_unoptimized.h *******
*********************************/
struct SIMD_None {
inline static int sub(int a, int b) {
std::cout << "NONE" << std::endl;
return a - b;
}
};
/**********************************
** functions_neon64.h ************
*********************************/
struct SIMD_Neon64 : SIMD_None {
inline static int sub(int a, int b) {
std::cout << "Neon64" << std::endl;
return a - b;
}
};
/**********************************
** functions_neon128.h ***********
*********************************/
struct SIMD_Neon128 : SIMD_Neon64 {
inline static int sub(int a, int b) {
std::cout << "Neon128" << std::endl;
return a - b;
}
};
/**********************************
** main.cpp **********************
*********************************/
#include "functions.h"
int main() {
SIMD::sub(2, 3);
}
这将输出 Neon128
.
优点:
- 实施中不需要
#ifdef
个 header 个文件
- 不需要调度函数,编译器会自动选择最好的
- 不需要额外的函数参数
缺点:
- 您需要更改对函数的所有调用并在它们前面加上
SIMD::
- 你需要把所有的函数都包裹在struct的里面并且使用继承,所以有点复杂
3。使用模板专业化
如果您有所有可能的 SIMD 实现的枚举,例如:
enum class SIMD_Type {
Min, // Dummy Value -> No Implementation found
None,
Neon64,
Neon128,
SSE3,
AVX2,
Max // Dummy Value -> Search downwards from here
};
您可以使用它(递归地)遍历它们,直到找到一个专门化的,例如:
template<SIMD_Type type = SIMD_Type::Max>
inline int add(int a, int b) {
constexpr SIMD_Type nextType = static_cast<SIMD_Type>(static_cast<int>(type) - 1);
return add<nextType>(a, b);
}
template<>
inline int add<SIMD_Type::Neon64>(int a, int b) {
std::cout << "NEON!" << std::endl;
return a + b;
}
这里调用add(1, 2)
会先调用add<SIMD_Type::Max>
,后者又会调用add<SIMD_Type::AVX2
、add<SIMD_Type::SSE3>
、add<SIMD_Type::Neon128>
,然后调用到 add<SIMD_Type::Neon64>
将调用专业化,因此递归在这里停止。
如果你想让它更安全一点(以防止长模板 instaciation 链),你可以为每个函数添加一个特化,如果它找不到任何特化就停止递归,例如:godbolt example
template<>
inline int add<SIMD_Type::Min>(int a, int b) {
static_assert(SIMD_Type::Min == SIMD_Type::Min, "No implementation found!");
return {};
}
在你的情况下它可能是这样的:
#include <iostream>
/**********************************
** functions.h *******************
*********************************/
enum class SIMD_Type {
Min, // Dummy Value -> No Implementation found
None,
Neon64,
Neon128,
SSE3,
AVX2,
Max // Dummy Value -> Search downwards from here
};
#include "functions_stubs.h"
#include "functions_unoptimized.h"
#ifdef __ARM_NEON
#include "functions_neon64.h"
#endif
#ifdef __SSE3__
#include "functions_see3.h"
#endif
// etc...
/**********************************
** functions_stubs.h *************
*********************************/
template<SIMD_Type type = SIMD_Type::Max>
inline int add(int a, int b) {
constexpr SIMD_Type nextType = static_cast<SIMD_Type>(static_cast<int>(type) - 1);
return add<nextType>(a, b);
}
template<>
inline int add<SIMD_Type::Min>(int a, int b) {
static_assert(SIMD_Type::Min == SIMD_Type::Min, "No implementation found!");
return {};
}
/**********************************
** functions_unoptimized.h *******
*********************************/
template<>
inline int add<SIMD_Type::None>(int a, int b) {
std::cout << "NONE" << std::endl;
return a + b;
}
/**********************************
** functions_neon64.h ************
*********************************/
template<>
inline int add<SIMD_Type::Neon64>(int a, int b) {
std::cout << "NEON!" << std::endl;
return a + b;
}
/**********************************
** functions_neon128.h *******************
*********************************/
template<>
inline int add<SIMD_Type::Neon128>(int a, int b) {
std::cout << "NEON128!" << std::endl;
return a + b;
}
/**********************************
** main.cpp **********************
*********************************/
#include "functions.h"
int main() {
add(1, 2);
}
会输出 NEON128!
.
优点:
- 实现中不需要#ifdef header 个文件
- 调用者不需要修改
缺点:
- 需要一个递归调用自身的额外调度函数(直到它达到一个特化)
- 编译器可能不会优化所有递归调用(尽管大多数编译器可能会)
大多数编译器还为您提供了一种强制内联某些函数的方法 (__attribute__((always_inline))
/ __forceinline
),您可以添加函数基模板以确保所有递归调用实际上都被内联。
- 可选地需要另一个函数来停止递归实例化(不是严格要求,编译器会在某个时候停止递归实例化)
4。每个函数一个文件
这是迄今为止最简单的选择 - 只需将每个函数(或类似函数的 collection)放入一个文件中,然后在其中执行 #ifdef
。
这样您就可以在单个文件中拥有 SIMD 的所有功能及其专门化,这也应该使编辑更加容易。
例如:
/**********************************
** functions.h *******************
*********************************/
#include "functions_add.h"
#include "functions_sub.h"
// etc...
/**********************************
** functions_add.h ***************
*********************************/
#ifdef __SSE3__
// SSE3
int add(int a, int b) {
return a + b;
}
#elifdef __ARM_NEON
// NEON
int add(int a, int b) {
return a + b;
}
#else
// Fallback
int add(int a, int b) {
return a + b;
}
#end
/**********************************
** functions_sub.h ***************
*********************************/
#ifdef __SSE3__
// SSE3
int sub(int a, int b) {
return a - b;
}
#elifdef __ARM_NEON_128
// NEON 128
int sub(int a, int b) {
return a - b;
}
#else
// Fallback
int sub(int a, int b) {
return a - b;
}
#end
优点:
- 该函数及其所有特化都在一个文件中,因此确定调用哪个函数要容易得多
- 只要您不在单个文件中塞入太多函数,就很容易实现和维护
缺点:
- 可能有很多 header 个文件
#ifdef
需要在每个header 中重复
这确实是一个 open-ended/subjective 问题,但我正在寻找关于如何“组织”相同功能的多个替代实现的不同想法。
我有一组函数,每个函数都有特定于平台的实现。具体来说,它们各自针对特定的 SIMD 类型具有不同的实现:NEON(64 位)、NEON(128 位)、SSE3、AVX2 等(以及一种非 SIMD 实现)。
所有函数都有非 SIMD 实现。并非所有函数都专用于每种 SIMD 类型。
目前,我有一个整体文件,它使用一堆#ifdef 来实现特定的 SIMD 专业化。当我们只将一些函数专门化为一种或两种 SIMD 类型时,它就起作用了。现在,它变得笨重了。
实际上,我需要一些功能类似于 virtual/override 的东西。非 SIMD 实现在基础 class 中实现,SIMD 特化(如果有)将覆盖它们。但我不想要实际的运行时多态性。此代码对性能至关重要,许多函数可以(并且应该)内联。
沿着这些路线的东西将完成我需要的东西(这仍然是一团乱麻的#ifdefs)。
// functions.h
void function1();
void function2();
#ifdef __ARM_NEON
#include "functions_neon64.h"
#elif __SSE3__
#include "functions_sse3.h"
#endif
#include "functions_unoptimized.h"
// functions_neon64.h
#ifndef FUNCTION1_IMPL
#define FUNCTION1_IMPL
void function1() {
// NEON64 implementation
}
#endif
// functions_sse3.h
#ifndef FUNCTION2_IMPL
#define FUNCTION2_IMPL
void function2() {
// SSE3 implementation
}
#endif
// functions_unoptimized.h
#ifndef FUNCTION1_IMPL
#define FUNCTION1_IMPL
void function1() {
// Non-SIMD implementation
}
#endif
#ifndef FUNCTION2_IMPL
#define FUNCTION2_IMPL
void function2() {
// Non-SIMD implementation
}
#endif
有人有更好的主意吗?
以下只是我在思考时想到的一些想法 - 可能还有我不知道的更好的解决方案。
1。 Tag-Dispatch
使用 Tag-Dispatch 您可以定义编译器应考虑的函数顺序,例如在这种情况下是
AVX2 -> SSE3 -> Neon128 -> Neon64 -> None
将使用此链中存在的第一个实现:godbolt example
/**********************************
** functions.h *******************
*********************************/
struct SIMD_None_t {};
struct SIMD_Neon64_t : SIMD_None_t {};
struct SIMD_Neon128_t : SIMD_Neon64_t {};
struct SIMD_SSE3_t : SIMD_Neon128_t {};
struct SIMD_AVX2_t : SIMD_SSE3_t {};
struct SIMD_Any_t : SIMD_AVX2_t {};
#include "functions_unoptimized.h"
#ifdef __ARM_NEON
#include "functions_neon64.h"
#endif
#ifdef __SSE3__
#include "functions_see3.h"
#endif
// etc...
#include "functions_stubs.h"
/**********************************
** functions_unoptimized.h *******
*********************************/
inline int add(int a, int b, SIMD_None_t) {
std::cout << "NONE" << std::endl;
return a + b;
}
/**********************************
** functions_neon64.h ************
*********************************/
inline int add(int a, int b, SIMD_Neon64_t) {
std::cout << "NEON!" << std::endl;
return a + b;
}
/**********************************
** functions_neon128.h ***********
*********************************/
inline int add(int a, int b, SIMD_Neon128_t) {
std::cout << "NEON128!" << std::endl;
return a + b;
}
/**********************************
** functions_stubs.h *************
*********************************/
inline int add(int a, int b) {
return add(a, b, SIMD_Any_t{});
}
/**********************************
** main.cpp **********************
*********************************/
#include "functions.h"
int main() {
add(1, 2);
}
这将输出 NEON128!
,因为这是本例中的最佳匹配。
优点:
- 实施中不需要
#ifdef
个 header 个文件 - 调用者不需要修改
缺点:
- 您需要为每个实现添加一个额外的参数
- 需要 dispatch-function 来提供额外的参数
(理论上,您可以通过在调用该函数的任何地方添加, SIMD_Any_t{}
来摆脱该函数,但这需要大量工作)
2。将函数放入 classes 并使用名称查找来选择正确的函数
例如:
struct None { inline static int add(int a, int b) { return a + b; } };
struct Neon64 : None { inline static int add(int a, int b) { return a + b; } };
struct Neon128 : Neon64 {};
struct SIMD : Neon128 {};
// Usage:
int r = SIMD::add(1, 2);
因为 child classes 可以隐藏其 base-classes 的成员,这不是歧义。 (总是会调用实现给定方法的 most-derived class,因此您可以订购您的实现)
对于您的示例,它可能如下所示:godbolt example
#include <iostream>
/**********************************
** functions.h *******************
*********************************/
#include "functions_unoptimized.h"
#ifdef __ARM_NEON
#include "functions_neon64.h"
#else
struct SIMD_Neon64 : SIMD_None {};
#endif
#ifdef __ARM_NEON_128
#include "functions_neon128.h"
#else
struct SIMD_Neon128 : SIMD_Neon64 {};
#endif
// etc...
struct SIMD : SIMD_Neon128 {};
/**********************************
** functions_unoptimized.h *******
*********************************/
struct SIMD_None {
inline static int sub(int a, int b) {
std::cout << "NONE" << std::endl;
return a - b;
}
};
/**********************************
** functions_neon64.h ************
*********************************/
struct SIMD_Neon64 : SIMD_None {
inline static int sub(int a, int b) {
std::cout << "Neon64" << std::endl;
return a - b;
}
};
/**********************************
** functions_neon128.h ***********
*********************************/
struct SIMD_Neon128 : SIMD_Neon64 {
inline static int sub(int a, int b) {
std::cout << "Neon128" << std::endl;
return a - b;
}
};
/**********************************
** main.cpp **********************
*********************************/
#include "functions.h"
int main() {
SIMD::sub(2, 3);
}
这将输出 Neon128
.
优点:
- 实施中不需要
#ifdef
个 header 个文件 - 不需要调度函数,编译器会自动选择最好的
- 不需要额外的函数参数
缺点:
- 您需要更改对函数的所有调用并在它们前面加上
SIMD::
- 你需要把所有的函数都包裹在struct的里面并且使用继承,所以有点复杂
3。使用模板专业化
如果您有所有可能的 SIMD 实现的枚举,例如:
enum class SIMD_Type {
Min, // Dummy Value -> No Implementation found
None,
Neon64,
Neon128,
SSE3,
AVX2,
Max // Dummy Value -> Search downwards from here
};
您可以使用它(递归地)遍历它们,直到找到一个专门化的,例如:
template<SIMD_Type type = SIMD_Type::Max>
inline int add(int a, int b) {
constexpr SIMD_Type nextType = static_cast<SIMD_Type>(static_cast<int>(type) - 1);
return add<nextType>(a, b);
}
template<>
inline int add<SIMD_Type::Neon64>(int a, int b) {
std::cout << "NEON!" << std::endl;
return a + b;
}
这里调用add(1, 2)
会先调用add<SIMD_Type::Max>
,后者又会调用add<SIMD_Type::AVX2
、add<SIMD_Type::SSE3>
、add<SIMD_Type::Neon128>
,然后调用到 add<SIMD_Type::Neon64>
将调用专业化,因此递归在这里停止。
如果你想让它更安全一点(以防止长模板 instaciation 链),你可以为每个函数添加一个特化,如果它找不到任何特化就停止递归,例如:godbolt example
template<>
inline int add<SIMD_Type::Min>(int a, int b) {
static_assert(SIMD_Type::Min == SIMD_Type::Min, "No implementation found!");
return {};
}
在你的情况下它可能是这样的:
#include <iostream>
/**********************************
** functions.h *******************
*********************************/
enum class SIMD_Type {
Min, // Dummy Value -> No Implementation found
None,
Neon64,
Neon128,
SSE3,
AVX2,
Max // Dummy Value -> Search downwards from here
};
#include "functions_stubs.h"
#include "functions_unoptimized.h"
#ifdef __ARM_NEON
#include "functions_neon64.h"
#endif
#ifdef __SSE3__
#include "functions_see3.h"
#endif
// etc...
/**********************************
** functions_stubs.h *************
*********************************/
template<SIMD_Type type = SIMD_Type::Max>
inline int add(int a, int b) {
constexpr SIMD_Type nextType = static_cast<SIMD_Type>(static_cast<int>(type) - 1);
return add<nextType>(a, b);
}
template<>
inline int add<SIMD_Type::Min>(int a, int b) {
static_assert(SIMD_Type::Min == SIMD_Type::Min, "No implementation found!");
return {};
}
/**********************************
** functions_unoptimized.h *******
*********************************/
template<>
inline int add<SIMD_Type::None>(int a, int b) {
std::cout << "NONE" << std::endl;
return a + b;
}
/**********************************
** functions_neon64.h ************
*********************************/
template<>
inline int add<SIMD_Type::Neon64>(int a, int b) {
std::cout << "NEON!" << std::endl;
return a + b;
}
/**********************************
** functions_neon128.h *******************
*********************************/
template<>
inline int add<SIMD_Type::Neon128>(int a, int b) {
std::cout << "NEON128!" << std::endl;
return a + b;
}
/**********************************
** main.cpp **********************
*********************************/
#include "functions.h"
int main() {
add(1, 2);
}
会输出 NEON128!
.
优点:
- 实现中不需要#ifdef header 个文件
- 调用者不需要修改
缺点:
- 需要一个递归调用自身的额外调度函数(直到它达到一个特化)
- 编译器可能不会优化所有递归调用(尽管大多数编译器可能会)
大多数编译器还为您提供了一种强制内联某些函数的方法 (__attribute__((always_inline))
/__forceinline
),您可以添加函数基模板以确保所有递归调用实际上都被内联。 - 可选地需要另一个函数来停止递归实例化(不是严格要求,编译器会在某个时候停止递归实例化)
4。每个函数一个文件
这是迄今为止最简单的选择 - 只需将每个函数(或类似函数的 collection)放入一个文件中,然后在其中执行 #ifdef
。
这样您就可以在单个文件中拥有 SIMD 的所有功能及其专门化,这也应该使编辑更加容易。
例如:
/**********************************
** functions.h *******************
*********************************/
#include "functions_add.h"
#include "functions_sub.h"
// etc...
/**********************************
** functions_add.h ***************
*********************************/
#ifdef __SSE3__
// SSE3
int add(int a, int b) {
return a + b;
}
#elifdef __ARM_NEON
// NEON
int add(int a, int b) {
return a + b;
}
#else
// Fallback
int add(int a, int b) {
return a + b;
}
#end
/**********************************
** functions_sub.h ***************
*********************************/
#ifdef __SSE3__
// SSE3
int sub(int a, int b) {
return a - b;
}
#elifdef __ARM_NEON_128
// NEON 128
int sub(int a, int b) {
return a - b;
}
#else
// Fallback
int sub(int a, int b) {
return a - b;
}
#end
优点:
- 该函数及其所有特化都在一个文件中,因此确定调用哪个函数要容易得多
- 只要您不在单个文件中塞入太多函数,就很容易实现和维护
缺点:
- 可能有很多 header 个文件
#ifdef
需要在每个header 中重复