Some Notes about Working on SIMD

22.4.18

首先是在研究 split 相关的编译错误时得到的结论: 在变长参数包非尾置参数的时候, 无法显式指定变长参数包之后的参数(而只能依靠 deduction), 因为编译器无法判断变长参数包到哪里截止。因此测试中应将 split<1, 2, 3, _Tp, _Abi>(data) 修改为 split<1, 2, 3>(data)。(也就是说这并不是一个问题, 而是 by design 如此)

其次是, 对于这样的两个 split 函数(一个是 split into tuple, 一个是 split into array)

1
2
3
4
5
6
7
8
9
10
11
12
// template<size_t... Sizes, class T, class Type>
template<class T, class Type, size_t... Sizes>
std::enable_if_t<
((0 + ... + Sizes) == simd_size_v<T, Type>),
std::tuple<simd<T, fixed_size<Sizes>>...>
> split(const simd<T, Type>&) noexcept;

template<class V, class Type>
std::enable_if_t<
(simd_size_v<typename V::value_type, Type> % V::size() == 0),
std::array<V, simd_size_v<typename V::value_type, Type> / V::size()>
> split(const simd<typename V::value_type, Type>&) noexcept;

在调用 split<simd<int, fixed_size<2>>, fixed_size<6>>(a) 时会出现编译错误(预期行为是第一个 split 根据 SFINAE 被忽略, 第二个函数成功匹配)。但实际上在对第一个函数进行 instantiation 的时候, 会进行对其返回类型的实例化:

1
2
3
4
5
6
7
8
9
std::enable_if_t<
((0 + ... + Sizes) == simd_size_v<T, Type>),
std::tuple<simd<T, fixed_size<Sizes>>...>
>
=>
std::enable_if_t<
((0) == simd_size_v<simd<int, fixed_size<2>>, fixed_size<6>>),
std::tuple<simd<simd<int, fixed_size<2>>, fixed_size<>>>
>

但这里 simd_size_v 又是另外一个单独的模板变量, 所以会进一步 instantiate 它:

1
simd_size_v<T, Type> => simd_size_impl<T, Type>::value

这个时候会出现错误: no member named 'value' in 'simd_size_impl<simd<int, fixed_size<2>>, fixed_size<6>>', 但注意到此时所处的阶段已经不是对 split 的实例化了, 而是被其调用的一个子过程, 是在 simd_size_v 的实例化阶段。根据 cppreference:

Only the failures in the types and expressions in the immediate context of the function type or its template parameter types or its explicit specifier (since C++20) are SFINAE errors. If the evaluation of a substituted type/expression causes a side-effect such as instantiation of some template specialization, generation of an implicitly-defined member function, etc, errors in those side-effects are treated as hard errors. (From SFINAE - cppreference)

在 C++ 标准中对于 immediate context 的定义可以感性理解为「在实例化函数模板过程中直接产生的错误」, 即如果错误是在实例化函数模板时直接产生的, 那么才是一个可以被忽略的 SFINAE 错误;而如果是间接产生的错误, 如此处错误产生于 实例化函数模板时产生的一个间接调用的子过程, 就不处于 immediate context 中, 所以是一个 hard error, 无法被忽略, 会直接报错。
所以解决这种问题的方法就是手动消除这一子过程, 让可能出现的错误只产生于 immediate context 中。对应到此处就是将函数签名中的 simd_size_v<T, Type> 手动替换为 simd_size_impl<T, Type>::value

值得一提的另外一点是, 如果将第一个 split 函数的模板参数顺序修改为注释所示, 那么也可以解决这个编译错误。这其实与开头提到的内容是对应的,如果将参数包放到首位的话,那么编译器在匹配模板参数这个阶段就会 fail,因为它并不知道应该如何将一个类型与 size_t 类型的参数包相匹配,从而接下来的实例化也不会进行,错误也不会产生。

Reference