C++ 使用分治减小模板递归深度
起因
C++14 引入 STL 的 make_index_sequence 可以生成一个类型为 std::size_t,0 到 N-1 的编译期序列,我们可以这样使用它:
代码
//利用函数参数推导提取序列
template<std::size_t... Seq>
void foo(std::index_sequence<Seq...>)
{
//使用Seq
}
//利用模板特化提取序列
template<typename>
struct extract_sequence;
template<std::size_t... Seq>
struct extract_sequence<std::index_sequence<Seq...>>
{
//使用Seq
};
foo(std::make_index_sequence<5>{});//foo内部得到 {0, 1, 2, 3, 4}
extract_sequence<std::make_index_sequence<3>>;//extract_sequence内部得到 {0, 1, 2}
可是在我的程序中,我想创建一个等差数列,std::make_index_sequence 无法直接拿来使用(其实可以间接使用,后文会提到)。于是我编写了一个自己的版本:
代码
//主模板,递归实例化
template<std::size_t Base,std::size_t Step, std::size_t Count, std::size_t... Seq>
struct make_arithmetic_progression : make_arithmetic_progression<Base + Step, Step, Count-1, Seq..., Base>
{
};
//递归终点特化版本
template<std::size_t Base, std::size_t Step, std::size_t... Seq>
struct make_arithmetic_progression<Base, Step, 0, Seq...>
{
using type = std::index_sequence<Seq...>;
};
template<std::size_t Base, std::size_t Step, std::size_t Count>
using arithmetic_progression = typename make_arithmetic_progression<Base, Step, Count>::type;
foo(arithmetic_progression<1, 2, 3>{});//foo内部得到 {1, 3, 5}
extract_sequence<arithmetic_progression<3, 3, 3>>;//extract_sequence内部得到 {3, 6, 9}
问题
这个实现看起来没什么问题,可以正确地生成任意指定的等差数列。但是在一个偶然的情况下,我在创建一个很长的数列时,遇到以下错误:
模板递归实例化深度超过限制了,查阅了一下资料,获知各家编译器都对这个深度有一个默认限制,GCC 和 clang 都是 1024,MSVC 是 499。
可以使用编译参数来指定这个限制:
代码
-ftemplate-depth=N //GCC 和 clang
/Zc:templateDepth=N //MSVC
但这显然是下策,每次编译这个模板都需要在编译时加上额外的指令,非常麻烦。
解决
自定义方式
我们知道有个排序算法叫归并排序,将长列表不断拆分成最小单元后合并。我们的这个数列实现也可以直接借鉴这个思路,拆分数组后合并:
代码
//list拼接类主模板
template<typename,typename>
struct index_list_concat;
//list拼接类特化版本
template<std::size_t... Seq1, std::size_t... Seq2>
struct index_list_concat<std::index_sequence<Seq1...>, std::index_sequence<Seq2...>>
{
using type = std::index_sequence<Seq1..., Seq2...>;
};
//辅助分割主模板
template<std::size_t Base, std::size_t Step, std::size_t Count>
class make_arithmetic_progression
{
static constexpr std::size_t HalfCount = Count / 2;
static constexpr std::size_t HalfBase = Base + HalfCount * Step;
public:
using type = typename index_list_concat<typename make_arithmetic_progression<Base, Step, HalfCount>::type,
typename make_arithmetic_progression<HalfBase, Step, Count - HalfCount>::type>::type;
};
//拆分为1个元素特化版本
template<std::size_t Base, std::size_t Step>
class make_arithmetic_progression<Base, Step, 1>
{
public:
using type = index_list<Base>;
};
//拆分为2个元素特化版本
template<std::size_t Base, std::size_t Step>
class make_arithmetic_progression<Base, Step, 2>
{
public:
using type = index_list<Base, Base + Step>;
};
template<std::size_t Base, std::size_t Step, std::size_t Count>
using arithmetic_progression = typename make_arithmetic_progression<Base, Step, Count>::type;
这样对于长度为 Count 的序列,模板递归实例化的深度变为 $log_2Count$,按照 MSVC 的默认最大深度 499 来算,也能支持到 $2^{499}$ 长度的序列,这无论如何都够用了,更别说 GCC 和 clang 的 1024 了。
借用标准库
上文说到可以间接使用标准库来实现这个等差数列模板。在我们初中的时候就知道,可以用公差为 1 的等差数列生成任意的其他等差数列:
$$
\begin{aligned}
a_n&=n (n=0,1,2,...)
\\\\
b_n&=a_n*d+k (n=0,1,2,...)//d为公差,k为首项
\end{aligned}
$$
结合 C++17 的折叠表达式,我们可以构建这样的模板:
代码
//辅助推导函数
template<std::size_t Base, std::size_t Step, std::size_t... Seq>
std::index_sequence<(Base + Step * Seq)...> arithmetic_progression_helper(std::index_sequence<Seq...>);
template<std::size_t Base, std::size_t Step, std::size_t Count>
using arithmetic_progression = decltype(arithmetic_progression_helper<Base, Step>(std::make_index_sequence<Count>{}));
这个模板只是在 STL 的 index_sequence 上做了一次包装,更加简洁。
优劣
在我对上述两种实现进行测试时,发现在实例化超长的数列时,使用 STL 的版本无论在速度上还是内存消耗上都显著优于自定义版本。
比如在我的电脑上使用 clang 编译,实例化一个长度为 100000 的数列。STL 版本的实现编译耗时 1 秒左右,内存仅占用数十 MB;而自定义版本的编译却需要 10 秒左右,内存占用超过 1.2GB。
为什么相差如此悬殊?我于是很好奇 STL 是如何实现 make_index_sequence 的,找到源码后跟进去一瞧,它其实是 __make_integer_seq<std::size_t, N>。大家都知道,前导两个下划线的 STL 模板是内部实现版本,它们要么由编译器支持没有源码,要么有源码但不建议直接使用,这个 __make_integer_seq 属于前者。这些由编译器支持的模板大部分属于用户端不可能实现或者实现极为复杂,而在编译器端实现会相对简单并且高效的。
这提醒我们,对于标准库能够直接或间接支持的模块,应该优先选择使用标准库来实现,它们会帮我们最大化地利用资源。不过在学习过程中,编写自己的版本也是很有价值的,能够让我们深入细节,接触优秀的编码思想和优化策略。
总结
使用分治来降低模板递归实例化深度这一策略,是我以前从没考虑过的,虽然它在本次编码中被最终舍弃,但是日后说不定有用武之地。本篇学习记录梳理了我在逐步实现和改进等差数列模板的思路,总结如下:
1. 学习过程中不断造轮子加验证是获取和掌握知识的有效途径;
2. 在实际项目中,尽量使用经过大众检验的库有助于项目的稳定高效。
对于第二条,如果为了一个模块而引入一整个大型库的依赖,仍须仔细斟酌。这与本篇笔记的内容无关,但在实际工作中,如果每个人都为了实现一个简单模块而肆无忌惮的引入三方依赖,造成团队项目仓库中一大半是三方库内容的情况还是很让人不舒服的。
C++ 使用分治减小模板递归深度的更多相关文章
- 关于python最大递归深度 - 998
今天LeetCode的时候暴力求解233 问题: 给定一个整数 n,计算所有小于等于 n 的非负数中数字1出现的个数. 例如: 给定 n = 13, 返回 6,因为数字1出现在下数中出现:1,10,1 ...
- Python的递归深度
RuntimeError: maximum recursion depth exceeded while calling a Python object 大意是调用 Python 对象时超出最大深度限 ...
- Python的递归深度问题
Python的递归深度问题 1.Python默认的递归深度是有限制的,当递归深度超过默认值的时候,就会引发RuntimeError.理论在997. 2.解决方法:最大递归层次的重新调整,解决方式是手工 ...
- java基础知识回顾之javaIO类--File类应用:递归深度遍历文件
代码如下: package com.lp.ecjtu.File.FileDeepList; import java.io.File; public class FileDeepList { /** * ...
- 【文文殿下】【洛谷】分治NTT模板
题解 可以计算每一项对后面几项的贡献,然后考虑后面每一项,发现这是一个卷积,直接暴力NTT就行了,发现它是一个有后效性的,我们选择使用CDQ分治. Tips:不能像通常CDQ分治一样直接 每次递归两边 ...
- 分治FFT模板
题目链接:https://www.luogu.org/problemnew/show/P4721 总结了一下蒟蒻FFT/NTT容易写错的地方: 1.rev数组求错. 2.cdq注意顺序:先递归 ...
- CDQ 分治算法模板
CDQ分治 1.三维偏序问题:三维偏序(陌上花开) #include<bits/stdc++.h> #define RG register #define IL inline #defin ...
- CDQ分治嵌套模板:多维偏序问题
CDQ分治2 CDQ套CDQ:四维偏序问题 题目来源:COGS 2479 偏序 #define LEFT 0 #define RIGHT 1 struct Node{int a,b,c,d,bg;}; ...
- JavaScript递归深度问题
递归是有限的东西: function fact(num) { if (num <= 1) { return 1; } else { return fact(num - 1); } } 测试结果是 ...
- 原生JavaScript实现JSON合并(递归深度合并)
// 遇到相同元素级属性,以(minor)为准 // 不返还新Object,而是main改变 function mergeJSON(minor, main) { for(var key in mino ...
随机推荐
- 使用ajax来进行登录验证
servlet: 1 @WebServlet("/login.do") 2 public class AjaxLoginServlet extends HttpServlet { ...
- [书籍精读]《基于MVC的JavaScript Web富应用开发》精读笔记分享
写在前面 书籍介绍:<JavaScript异步编程>讲述基本的异步处理技巧,包括PubSub.事件模式.Promises等,通过这些技巧,可以更好的应对大型Web应用程序的复杂性,交互快速 ...
- 指标+AI:迈向智能化,让指标应用更高效
近日,以"Data+AI,构建新质生产力"为主题的袋鼠云春季发布会圆满落幕,大会带来了一系列"+AI"的数字化产品与最新行业沉淀,旨在将数据与AI紧密结合,打破 ...
- 六、Linux系统 DRM调试工具modetest
4.20.modetest(调试 DRM) modetest 是 libdrm 提供的一个测试工具,用于调试 DRM(Direct Rendering Manager) 设备,它能够列出可用的显示设备 ...
- 2025 FJCPC 复建 VP
按开题顺序写 \(BCDEFGHIJKLA(D?)\),\(M\) 送的不写 B 首先发现铜铁本质等价(铜铁的转换不影响 \(val\) ),所以考虑枚举最后金和银的数量 \(gold, silver ...
- .NET 8性能优化全攻略:让你的应用飞起来!
大家好!我是.NET修仙日记的掌门人,今天我们来聊聊.NET 8的性能优化技巧.随着.NET 8的发布,微软带来了更多性能改进的可能性.无论你是开发Web应用.微服务还是桌面程序,这些优化技巧都能让你 ...
- vs 开发 qt 遇到 无法找到 Visual Studio 2010 的生成工具(平台工具集 =“v100”) 解决方案
参考链接 相关解决方案
- Django+Celery 进阶:动态定时任务的添加、修改与智能调度实战
一.Celery定时任务 Celery Beat 介绍 Celery Beat 是 Celery 框架的一个内置组件,专门用于定时任务调度.它可以按照预设的时间规则(如固定间隔.特定时间点.CRON ...
- ETL多流数据合并的使用技巧
在ETLCloud中,多流数据合并是指将来自不同源的数据流实时或批量地合并到一个统一的数据流或数据集.这对于确保数据一致性.减少数据冗余和提高查询效率至关重要.通过合并多流数据,可以实现对多源数据的综 ...
- ETLCloud结合kafka的数据集成
一.ETLCloud中实时数据集成的使用 在ETLCloud中数据集成有两种方式,一种是离线数据集成,另一种便是我们今天所要介绍的实时数据集成了,两者的区别从名字便可以得知,前者处理的数据是离线的没有 ...