【问题标题】:Function template accepting either a template value parameter or a function argument接受模板值参数或函数参数的函数模板
【发布时间】:2021-10-18 22:51:29
【问题描述】:

问题

有没有办法编写一个模板化函数,该函数接受模板值参数(用于静态已知值)或经典函数参数(用于动态值)? 我会想象这样的可调用的东西,允许编译器优化“静态”版本。

static_dynamic_fun<T>(a, b, default_fun<T>);   // Use function argument
static_dynamic_fun<T, default_fun<T>>(a, b);   // Use template parameter

注意:我们可以使用 C++20(尽管使用 C++11 甚至 C++14 的解决方案会对更广泛的受众有用)。

上下文

我有一个模板函数,其参数之一是静态已知函数:

  template<typename T, auto dist = default_fun<T>>
  T static_fun(const T* a, const T* b){...}

dist 参数在多个循环中被大量使用(称为数十亿次) - 将它放在模板中允许编译器内联函数(通常很小)。 我们还需要一个带有动态“dist”函数的代码版本(例如,由 lambda 制成):

  template <typename T>
  using funtype = std::function<T(const T* a, const T* b)>

  template<typename T>
  T dynamic_fun(const T* a, const T* b, const funtype<T>& dist = default_fun<T>){...}

问题是我们有重复的代码。我检查了上面的程序集输出:即使默认函数是静态已知的,调用也没有内联。

我试过这个,希望编译器会“看到”优化机会, 但无济于事。

  template<typename T, auto d = default_fun<T>>
  T static_dynamic_fun(const T* a, const T* b, const funtype<T>& dist=d){...}

我在 ASM 中跟踪了调用(“-fverbose-asm”GCC 标志)——因为我们通过 std::function,开销甚至大于对函数指针的间接调用(我们很高兴的成本当我们使用 lambda 之类的东西进行捕获时付费)。

# /usr/include/c++/11.1.0/bits/std_function.h:560:  return _M_invoker(_M_functor, std::forward<_ArgTypes>(__args)...);
    leaq    216(%rsp), %rsi #, tmp490
    movq    %r12, %rdx  # tmp666,
    movq    %r14, %rdi  # tmp650,
    call    *552(%rsp)  # MEM[(struct function *)_922]._M_invoke

** 编辑:添加一个最小的可重现示例 **

标题mre.hpp 定义:

  • 一个函数类型,funtype
  • 该类型的函数,argfun
  • argfun 的两个“用户”(请参阅​​下面的mre.cpp 了解用法)
    • static_fun,通过模板参数使用argfun
    • dynamic_fun,通过函数参数使用argfun
#include <functional>

template <typename T>
using funtype = std::function<T(const T* a, const T* b)>;

template <typename T>
T argfun(const T*a, const T*b){
    T d = a[0] - b[0];
    return d*d;
}

template <typename T, auto static_callme>
T static_fun(size_t s, const T* a, const T* b){
    T acc=0;
    for(size_t i=0; i<s; ++i){
        acc+=static_callme(a+i, b+i);
    }
    return acc;
}

template <typename T>
T dynamic_fun(size_t s, const T* a, const T* b, const funtype<T>& dynamic_callme){
    T acc=0;
    for(size_t i=0; i<s; ++i){
        acc+=dynamic_callme(a+i, b+i);
    }
    return acc;
}

文件mre.cpp

#include "mre.hpp"
#include <random>
#include <vector>
#include <iostream>


int main(){
    // Init random
    std::random_device rd;
    unsigned seed = rd();
    std::mt19937_64 prng(seed);

    // Random size for vectors
    size_t size = std::uniform_int_distribution<std::size_t>(10, 35)(prng);

    // Random vectors a and b of doubles [0, 1[
    std::uniform_real_distribution<double> udist{0.0, 1.0};
    auto generator = [&prng, &udist]() { return udist(prng); };
    std::vector<double> a(size);
    std::generate(a.begin(), a.end(), generator);
    std::vector<double> b(size);
    std::generate(b.begin(), b.end(), generator);

    // Static call
    double res_sta = static_fun<double, argfun<double>>(size, a.data(), b.data());
    std::cout << "size = " << size << " static call = " << res_sta << std::endl;

    // Dynamic call
    double res_dyn = dynamic_fun(size, a.data(), b.data(), argfun<double>);
    std::cout << "size = " << size << " dynamic call = " << res_dyn << std::endl;

    // Just to be sure:
    if(res_sta != res_dyn){
        std::cerr << "Error!" << std::endl;
        return 1;
    } else {
        std::cout << "Ok" << std::endl;
    }

    return 0;
}

我编译这个 g++ -std=c++20 mre.cpp -O3 -S -fverbose-asm

在asm文件mre.s中,搜索

  • static_callme: 看到对 argfun 的调用是内联的
  • dynamic_callme:看到调用通过std::function

我希望在没有代码重复的情况下同时拥有这两种行为。 static_fundynamic_fun 的 body 是一样的。

【问题讨论】:

  • 这里有一个minimal reproducible example 会很有用。我发现故事很难理解。
  • @PaulSanders 感谢您的评论;我添加了一个 MRE。
  • 尝试stackoverflow.com/a/39087660/1774667替换动态版本的std函数,然后静态调用动态,-O3。根据我的经验,函数视图对编译器来说比 std 函数更透明。
  • @Yakk-AdamNevraumont 谢谢!我试过了,它确实生成了一个“更轻”的 ASM 代码。但是,函数 argfun 仍然被调用而不是内联。

标签: c++ templates compile-time function-templates


【解决方案1】:

由于您已经使用模板,只需使用typename F 而不是std::function&lt;&gt;

template <typename T, typename F>
T static_dynamic_fun(size_t s, const T* a, const T* b, F f)
{
    T acc = 0;
    for (size_t i = 0; i != s; ++i) {
        acc += f(a + i, b + i);
    }
    return acc;
}
// and possibly, for default
template <typename T>
T static_dynamic_fun(size_t s, const T* a, const T* b)
{
    return static_dynamic_fun(s, a, b, default_f<T>{});
}

注意:您可能更喜欢传递 lambda 而不是函数指针,更容易内联。

【讨论】:

  • 确实,谢谢! (也许不太)令人惊讶的是,在这里不那么具体实际上更好 - 我想“让编译器弄清楚”是要走的路。编译器是否考虑到任何“f”都是它自己的 F 类型?我尝试使用具有相同签名的不同函数的多个调用,并且代码始终是专门的。
猜你喜欢
  • 1970-01-01
  • 2013-08-02
  • 2011-05-27
  • 2021-03-29
  • 2011-04-15
  • 2019-11-22
  • 1970-01-01
  • 2023-04-01
  • 1970-01-01
相关资源
最近更新 更多