C++ 矢量模板每组件操作

Posted

技术标签:

【中文标题】C++ 矢量模板每组件操作【英文标题】:C++ Vector Template Per-Component Operations 【发布时间】:2016-07-01 19:19:37 【问题描述】:

我正在重写我项目的向量数学部分,我想通过向量的类型和维数来概括向量。一个vector<T, N>代表一个T类型的N维向量。

template<typename T, int N>
struct vector 
    T data[N];
;

我需要重写许多数学函数,其中大部分将在每个组件的基础上运行。加法运算符的简单实现如下所示。

template<typename T, int N>
vector<T, N> operator+(vector<T, N> lhs, vector<T, N> rhs) 
    vector<T, N> result;
    for (int i = 0; i < N; i++) 
        result[i] = lhs[i] + rhs[i];
    
    return result;

我的问题:有没有办法(通过模板技巧?)在不使用 for 循环和临时变量的情况下实现这一点?我知道编译器很可能会展开循环并将其优化掉。我只是不喜欢以这种方式实现所有对性能至关重要的数学函数的想法。它们都将被内联并在标题中,因此拥有许多这些函数也会导致头文件变大。

我想知道是否有一种方法可以生成更优化的源代码。可能是一种像可变参数模板一样工作的方式。类似的东西。

template<typename T, int N>
vector<T, N> operator+(vector<T, N> lhs, vector<T, N> rhs) 
    return vector<T, N>(lhs[0] + rhs[0], lhs[1] + rhs[1]...);

【问题讨论】:

【参考方案1】:

一种方法是通过较低级别的“地图”函数:

这是一个完整的工作示例

#include <iostream>
#include <math.h>

template<typename T, int N>
struct vector 
    T data[N];
;

首先声明您的工作人员“映射”函数 - 我这里有 3 个 mapmap2foreach

template<typename T, int N, typename FN>
static void foreach(const vector<T,N> & vec, FN f) 
   for(int i=0; i<N ;++i) 
      f(vec.data[i]);
   


template<typename T, int N, typename FN>
static auto map(const vector<T,N> & vec, FN f) -> vector<decltype(f(T(0))), N> 
   vector<decltype(f(T(0))), N> result;
   for(int i=0; i<N ;++i) 
      result.data[i] = f(vec.data[i]);
   
   return result;


template<typename T1, typename T2, int N, typename FN>
static auto map2(const vector<T1,N> & vecA, 
                 const vector<T2,N> & vecB, 
                 FN f)
 -> vector<decltype(f(T1(0), T2(0))), N> 
   vector<decltype(f(T1(0), T2(0))), N> result;
   for(int i=0; i<N ;++i) 
      result.data[i] = f(vecA.data[i], vecB.data[i]);
   
   return result;

现在使用帮助程序通过 lambda 定义更高级别的函数。我将定义二进制 +、二进制 -、一元 - 和 e^x。哦,还有operator&lt;&lt;,所以我们可以看看发生了什么。

我很确定 operator+operator- 中使用的 lambda 有更好的替代品,但我不记得了

template<typename T, int N>
vector<T,N> operator+(const vector<T,N> &lhs, const vector<T,N> &rhs) 
  return map2(lhs, rhs, [](T a,T b)  return a+b; );


template<typename T, int N>
vector<T,N> operator-(const vector<T,N> &lhs, const vector<T,N> &rhs) 
  return map2(lhs, rhs, [](T a,T b)  return a-b; );


template<typename T, int N>
vector<T,N> operator-(const vector<T,N> &vec) 
  return map(vec, [](T a)  return -a; );


template<typename T, int N>
auto exp(const vector<T,N> &vec) -> vector<decltype(exp(T(0))), N> 
  return map(vec, [](T a)  return exp(a);  );


template<typename T, int N>
std::ostream & operator<<(std::ostream& os, const vector<T,N> &vec) 
  os<<"";
  foreach(vec, [&os](T v)  os<<v<<", ";  );
  os<<"";
  return os;

现在看看它们是如何工作的...

int main() 
  vector<int, 5> v1 = 1,2,3,4,5;
  vector<int, 5> v2 = 2,4,6,8,10;

  std::cout<<v1 << " + " << v2 << " = " << v1+v2<<std::endl;
  std::cout<<v1 << " - " << v2 << " = " << v1-v2<<std::endl;
  std::cout<<" exp( - " << v2 << " )= " << exp(-v1)<<std::endl;

【讨论】:

lambda 映射函数是一个有趣的想法。他们肯定会缩短代码的长度,但仍然依赖 for 循环。 for 循环的限制是编译时常量——编译器将展开它们。但是如果您发现函数是瓶颈,那么您可能想要使用更健壮且经过测试的数值向量类 - 或者手动专门处理瓶颈案例。【参考方案2】:

你可以这样做,我会为你指出一个解决方案(编译和运行)。您希望摆脱循环,最好通过内联它,希望编译器会为您优化。

在实践中,我发现指定所需的尺寸就足够了,即 N = 3、4、5,因为这允许对编译器的操作进行更细粒度的控制,而不是执行您要求的操作。但是,您可以使用递归和部分模板特化来实现您的运算符。我已经说明了加法。

所以不要这样:

template<typename T, int N>
vector<T, N> operator+(vector<T, N> lhs, vector<T, N> rhs) 
    vector<T, N> result;
    for (int i = 0; i < N; i++) 
        result[i] = lhs[i] + rhs[i];
    
    return result;

您需要有效执行此操作的代码:

   template<typename T, int N>
    vector<T, N> operator+(vector<T, N> lhs, vector<T, N> rhs) 
        vector<T, N> result;
        result[0] = lhs[0] + rhs[0];
        result[1] = lhs[1] + rhs[1];
        ...
        result[N-1] = lhs[N-1] + rhs[N-1];
        return result;
    

如果 N 为 1,很容易你只想要这个... 模板 向量运算符+(向量 lhs,向量 rhs) 矢量结果; 结果[0] = lhs[0] + rhs[0]; 返回结果;

如果 N 为 2,那么你只需要这个就很容易了... 模板 向量运算符+(向量 lhs,向量 rhs) 矢量结果; 结果[0] = lhs[0] + rhs[0]; 结果[1] = lhs[1] + rhs[1]; 返回结果;

最简单的方法是简单地将其定义为您期望使用的 N,而不是您正在寻找的答案,因为实际上您可能不需要超过 N=5 或 N=6,对吧?

但是,您也可以使用部分模板特化和递归来实现。考虑这个结构体,它递归调用自身,然后分配索引:

template<typename T, int N, int IDX>
struct Plus

    void operator()(vector<T,N>& lhs, vector<T,N>& rhs, vector<T,N>& result)
    
        Plus<T,N,IDX-1>()(lhs,rhs,result);
        result.data[IDX] = lhs.data[IDX] + rhs.data[IDX];
    
;

这个部分特化似乎什么都不做,但处理索引为 0 的情况并结束递归:

template<typename T, int N>
struct Plus<T,N,-1>

    void operator()(vector<T,N>& lhs, vector<T,N>& rhs, vector<T,N>& result)
    
        //noop
    
;

最后是 operator+ 的实现,它实例化 Plus 并调用它:

template<typename T, int N>
vector<T, N> operator+(vector<T, N> lhs, vector<T, N> rhs) 
    vector<T, N> result;
    Plus<T,N,N-1>()(lhs,rhs,result);
    return result;

您需要将其转换为运算符以使其更通用,但您明白了。然而,这对编译器来说很重要,即使它非常酷,在大型项目中也可能需要一段时间。在实践中,我发现手动键入您想要的重载或编写脚本代码来生成 C++ 会产生更可调试的体验,并且代码最终更易于阅读并且更易于编译器优化。更具体地说,如果您编写脚本来生成 C++,您可以首先包含 SIMD 内在函数,而不是让事情发生。

【讨论】:

我考虑过“模板循环”,因为没有更好的术语。我同意他们在编译器上可能有点粗糙。我最初的计划是将我的函数专门用于最常见的向量大小,而将我的通用 for 循环函数留给任何其他大小。【参考方案3】: 首先,编译器可能会展开循环。 其次,为了获得更好的性能,请通过 const 引用而不是值来传递参数以避免额外的副本。

为了回答您的问题,您可以使用std::index_sequence 就地构建,例如:

namespace detail


template<typename T, int N, std::size_t...Is>
vector<T, N> add(std::index_sequence<Is...>,
                 const vector<T, N>& lhs,
                 const vector<T, N>& rhs)

    return  (lhs[Is] + rhs[Is])... ;




template<typename T, int N>
vector<T, N> operator+(const vector<T, N>& lhs, const vector<T, N>& rhs) 
    return detail::add(std::make_index_sequence<N>, lhs, rhs);

Demo

【讨论】:

我目前通过 const 引用传递向量,但我计划最终进行一些分析,看看实际上是通过引用还是值传递它们更好。感谢您让我注意到 index_sequence。看起来它可能非常有用。

以上是关于C++ 矢量模板每组件操作的主要内容,如果未能解决你的问题,请参考以下文章

如何在自定义表格组件中使用子模板来显示每一行?

C++ 3D 矢量模板

具有父组件操作的角度更改子组件模板

C++ 中是不是有标准的 3d 矢量类

矢量化的HTML5拓扑图形组件设计

六十九Vue组件