线段树(Segment Tree)是一种将数组区间存储为树的数据结构 ^1。
时间复杂度
查询连续子数组 或 查询范围内的最小元素:O(logn)
最简单形式的线段树
高阶版本的线段树
TODO
在测试leetcode的 815.公交路线 一题时,发现一个现象,当我使用vector<vector<bool>>作为图的邻接矩阵的类型时,运行速度比使用vector<vector<int>>慢了近1倍。使用bool运行速度为600ms左右,使用int时运行速度为300ms左右。
以下是ChatGPT的回答:
在C++中,bool类型并不一定比int类型慢,但在某些情况下,它们的性能表现可能有所不同。以下是一些与性能差异相关的考虑:
1 | #使用 -x 和 -v 选项运行 main.sh 脚本。 |
注意:调试输出是通过 stderr ,在试图重定向时要注意。
1 | set -xv |
| Short notation | Long notation | Result |
|---|---|---|
| set -f | set -o noglob | Disable file name generation using metacharacters (globbing). |
| set -v | set -o verbose | Prints shell input lines as they are read. |
| set -x | set -o xtrace | Print command traces before executing command. |
参考链接
其他选项:
set +e:忽略错误继续运行(bash)。set -e:遇错停止执行(bash)。PS41 | PS4='+ ${LINENO}: ' |
可以搭配使用 PS4 环境变量,以在调试输出中打印命令所在的文件行号。
PS4 是一个 Bash 环境变量,用于定义调试模式下的提示符格式。
当你启用调试模式(通过 set -x)时,Bash 会在每一行执行之前打印调试信息,而 PS4 决定了这些调试信息的格式。$LINENO表示打印行号。
trap DEBUG设置DEBUG陷阱,该陷阱在每一条“简单语句”执行完毕之后会被调用一次。
1 | set -o functrace |
说明:监控my_var变量的变化,如果变化,则打印新值。
注意:实际测试中,我们发现trap监控变量变化似乎会延迟,而set -x打印的信息是可靠的。
参考:https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html
BASH_LINENO: 这是一个数组变量,用于表示当前执行的脚本或函数中每一行的行号。它特别适用于调试,因为它可以追踪到函数调用栈中每一层的行号。通过访问BASH_LINENO数组的不同索引,可以获取到不同函数调用层级对应的行号信息。LINENO: 获取当前脚本行号,而不涉及函数调用栈的追踪。FUNCNAME: 函数名BASH_SOURCE在当前代码根目录下创建.vscode/launch.json 文件,写入如下内容:
1 | { |
其中,”(gdb) Launch”可以用来连接 gdbserver。用 gdbserver 启动 executable:
1 | gdbserver peer_host:local_port prog [args] |
如果在点击”Run”按钮之前,在 VSCode 的 Debugger Terminal 写上命令(可以不按 Enter),那么在程序启动时
,该命令会被先执行。
Microsoft 的 C/C++ Extension Pack 跳转较慢,使用 clangd 会更快速。但是使用 clangd 后不能使用
cppdbg 类型来连接 gdbserver ,需要安装 Native Debug 插件,使用原始的 gdb 工具来连接 gdbserver 。
gdbserver 也可以使用 attach 的方式。
1 | gdbserver --attach :port PID |
.vscode/launch.json
1 | { |
1. Q: Configured debug type 'cppdbg' is not supported.
A: Please check if the "c/c++" extension is installed. (ref)
2. Q: VS Code Remote-SSH: The vscode server failed to start SSH
A: https://stackoverflow.com/questions/67976875/vs-code-remote-ssh-the-vscode-server-failed-to-start-ssh
3. Q: Where does VS Code store the data for its Local History feature?
A: https://stackoverflow.com/questions/72610147/where-does-vs-code-store-the-data-for-its-local-history-feature
4. vscode无法跳转到定义
A: when "C_Cpp.intelliSenseEngine" is set to "Tag Parser", Goto definition is working fine.
1 | $ which rpcgen |
Note:
% 可以用来转义某行,任何句首带有 % 的行将被视作字符串,直接放入到输出文件中。
注意:rpcgen 可能改变该行的位置,所以应该在输出文件中对这些行进行仔细检查。
rpcgen provides an additional preprocessing feature: any line that begins with a percent sign (%) is passed directly to the output file, with no action on the line’s content. Use caution because rpcgen does not always place the lines where you intend. Check the output source file and, if needed, edit it.
例如
1 | #include "abc.h" |
当执行 rpcgen infile 命令时,由于 “abc.h” 不存在,可能会报错。
但是如果在句首添加一个 % 符号,则可以绕过检查。
1 | %#include "abc.h" |
Free English Class! Topic: Hair! 🧔🏽💇✂️ (Lesson Only)
eyelashes
makeup
hairstyle/hairdo
get my hair done
hair gel
buzz cut
helmet
When you have a lot of hair, the helmet makes your hair flat.
hair colouring
From ChatGPT:
In general, when a program enters a signal handler in a multi-threaded environment, the behavior regarding other threads depends on how the signal handler is set up and the specific signal that is being handled.
使用脚本一键监查系统是否安装了 TBB
1 | #!/bin/bash |
从包管理器安装:
1 | sudo apt update |
用源码安装
如果需要最新版本,可以从 GitHub 获取 oneTBB
1 | git clone https://github.com/oneapi-src/oneTBB.git |
默认会安装到 /usr/local/include/ 和 /usr/local/lib/。
官方文档:https://uxlfoundation.github.io/oneTBB/
优点:
1 | tg.run(); // 非阻塞 |
概念澄清:
当 tg.wait() 时,调用线程会帮忙执行任务,具体是如何实现的?
任务队列(Task Queue)
tg.run() 会把所有待执行的任务放入一个共享的队列(通常是无锁队列或工作窃取队列)。
每个工作线程(包括调用线程)都可以从队列里取任务来执行。
调用线程参与执行
当调用线程进入 tg.wait() 时,它不会只是挂起等待,而是:
完成条件
tg.wait() 内部会维护一个 任务计数器(例如原子变量),记录剩余任务数。
避免死锁
调用线程必须能执行任务,否则如果它只是等待,而任务又依赖它执行,就会死锁。
所以 tg.wait() 的设计就是让调用线程也成为”工作线程”,保证任务能被及时调度。
| 特性 | std::thread | TBB |
|---|---|---|
| ** 线程数量 ** | 每个任务可能创建新线程 | 线程池固定线程数 |
| ** 调度开销 ** | 每次创建 / 销毁线程成本高 | 任务分配成本低,线程重用 |
| ** 负载均衡 ** | 需要手动管理任务分配 | 自动工作窃取,动态均衡 |
| ** 粒度控制 ** | 线程粒度粗,任务小不能充分利用 CPU | 任务粒度可以小,TBB 自动调度 |
| ** 锁竞争 ** | 共享队列 / 资源容易产生大量锁竞争 | 工作窃取减少锁竞争,性能更高 |
TBB 高效的核心原因:
所以,用 TBB 写 parallel_for 或 flow::graph 时,即使任务非常小,CPU 核心也能被充分利用,而直接 std::thread 很可能线程创建成本比任务本身还高,性能反而下降。
task_group 的本质
1 | #include <tbb/tbb.h> |
测试:
1 | $ mkdir build && cd build && cmake .. && make |
从这两行日志可以看出,arena 和 group 重用了同一个线程 ID ,说明它们同属于同一个全局线程池。
1 | Task arena thread 140667167639104 is running. |
进一步,我们发现全局线程池中的线程总数是自适应的,比如本例就是 10 个,既不是 task_group 的 8 个,
也不是 task_arena 的 4 个:
TODO
1 | $ cat result.txt | grep running | sort | uniq | wc -l |
TBB 是基于任务,不是基于线程。但是如果你想修改 TBB 的线程数,有两种方法:
TBB_NUM_THREADS 进行全局设置:1 | export TBB_NUM_THREADS=4 |
TODO: It doesn’t seem to work!
tbb::task_arena or tbb::task_scheduler_init (Deprecated) 进行线程隔离。TBB will use this setting locally within the scope of the tbb::task_arena.
1 | #include <tbb/pipeline.h> |
task_arena 默认 不会去窃取全局线程池的线程
tbb::task_arena 是 局部线程池的抽象,可以指定线程数量和优先级。
在 arena.execute(…) 里执行的任务:
优先使用 task_arena 自己的线程(如果 arena 里有空闲线程)
不会去窃取全局线程池的线程
也就是说,task_arena 内的任务和全局线程池是相对隔离的。
如果 task_arena 内的线程空闲不足,默认不会去全局线程池窃取线程。
但是 TBB 内部可能会将一些未使用的线程调度给 arena,但这属于内部优化,不等同于直接窃取整个全局线程池。
总体原则:arena 控制自己的线程数,不影响全局线程池。
| 特性 | 全局线程池 | task_arena |
|---|---|---|
| 线程池数量 | 默认一个 | 每个 arena 独立,可自定义线程数 |
| 窃取行为 | 工作窃取机制(线程之间互窃) | 默认只在 arena 内窃取,不窃取全局 |
| 嵌套并行 | 嵌套任务复用全局线程 | 嵌套任务复用 arena 线程 |
| 适用场景 | 大多数并行调用 | 局部控制线程数、避免与全局任务竞争 |
parallel_forAPI: parallel_for
my_parallel_for 模拟 parallel_for 的实现:测试结果:
1 | $ ./test_parallel_for |
可见,data 2 是由主线程处理的。也就是说,parallel_for 虽然被称为 a blocking parallel construt,但线程等待所有任务完成期间是非阻塞的,它还可以充当工作线程执行任务池中的任务。
代码模拟 parallel_for 的 wait :
当追求性能时,推荐以逻辑任务(logical tasks)而不是线程(threads)来编程,有以下原因:
TODO
每个线程都有自己的双端队列,头部称为 top (也称顶部),尾部称为 bottom (也称底部)。
队列的底部是队列的最深处(最末处),底部任务是最新的,顶部任务是最旧的。
深度优先有以下好处:
热点缓存命中:最新的任务的缓存是最热的,所以优先执行新任务。最小化空间:广度优先会同时创建指数级数量的共存节点,而深度优先虽然也会创建相同数量的节点,但是只有线性数目的节点会同时共存,因为它创建了其他就绪任务的栈。生产:当线程产生一个任务时,将其放置到线程自身所有的 deque 的尾部。
消费:当线程执行任务时,根据以下规则顺序选取一个任务:
规则 1 被称为 “任务调度绕行(Task Scheduler Bypass)”。
规则 2 是深度优先,这使得当前线程得以不断执行最新的任务直至其完成所有工作。
规则 3 是临时的广度优先,它将潜在的并行转化为实际的并行。
一个任务从产生到被执行涉及以下步骤:
- 将新任务加入线程的 deque 。
- 执行当前任务直至完成。
- 从线程 deque 获取一个任务执行,除非该任务被其他线程窃取走了。
其中,步骤 1 和 步骤 3 会引入不必要的 deque 操作,甚至更糟的是,允许窃取会损害局部性而不会增加显著的并行性。
任务调度器绕行技术可以直接指向下一个要被执行的任务,而不是生产该任务,从而避免了上述问题。
因为根据 “规则 1”,上一个任务产生的新任务会称为第一个备选任务。
此外,该技术几乎保证了该新任务被当前线程执行,而不是其他线程。
注意:当前唯一能使用该优化技术的是使用 tbb::task_group 。
Guiding Task Scheduler Execution
默认情况下,任务计划程序会尝试使用所有可用的计算资源。在某些情况下,您可能希望将任务计划程序配置为仅使用其中的一些资源。
注意:指导任务调度程序的执行可能会导致可组合性问题。
TBB 提供 task_arena 接口,通过以下方式指导任务在 arena (竞技场)内被执行:
如果当前线程被 parallel_for “阻塞”(不是真正的阻塞,只能称为 a blocking parallel construct),那么该线程被允许拿取第一个循环的任务来执行。这会导致即使是同一个线程内,也可出现乱序执行的情况。在大多数情况下,这没有什么危害。
但是少数情况可能出现错误,例如一个 thread-local 变量可能会在嵌套并行构造之外意外被更改:
在其它场景下,这种行为可能会导致死锁或其他问题。在这些情况下,需要更有力地保证线程内的执行次序。为此,TBB 提供了一些隔离并行构造的执行的方法,以使其任务不会干扰其他同时运行的任务。
其中一种方法是在单独的 task_arena 中执行内层循环:
然而,使用单独的 arena 进行工作隔离并不总是方便的,并且可能会产生明显的开销。为了解决这些缺点,TBB 提供 this_task_arena::isolate 函数,通过限制调用线程仅处理在函数对象范围内(也称为隔离区域)安排的任务,来隔离地运行一个用户提供的函数对象。
当一个线程进入一个任务等待调用或(等待)在一个隔离区域内的阻塞并行结构时,该线程只能执行在该隔离区域内生成的任务及其由其他线程生成的子任务(换句话说,即使子任务是由其他线程生成的,只要属于当前隔离区域,当前线程也可以执行这些任务)。线程被禁止执行任何外层任务或属于其他隔离区域的任务。
下面的示例展示了 this_task_arena::isolate 的使用,以保证在嵌套的并行结构调用时, thread-local 变量不会被意外修改:
** 补充:** 让我们通过一个简单的例子来说明隔离区域内其他线程如何生成子任务,并且这些子任务可以由当前线程执行。
假设我们有一个隔离区域,其中有两个线程:线程 A 和线程 B。我们在这个隔离区域内生成了一些任务,并且这些任务可能会生成子任务。
在这个例子中:
taskA 和 taskB 是在隔离区域内生成的任务。
taskA 生成了两个子任务 Subtask A1 和 Subtask A2。
taskB 生成了两个子任务 Subtask B1 和 Subtask B2。
假设线程 A 执行了 taskA,线程 B 执行了 taskB。在隔离区域内,线程 A 和线程 B 可以执行彼此生成的子任务。例如,线程 A 可以执行 Subtask B1 或 Subtask B2,而线程 B 可以执行 Subtask A1 或 Subtask A2,只要这些子任务属于同一个隔离区域。
严格来说,TBB 并不是完全无锁,但它尽量采用 无锁(lock-free)设计 来提高性能。下面详细说明:
a. 无锁设计的部分
工作窃取双端队列(deque)
并行算法(parallel_for, parallel_reduce)
轻量级任务对象(task)引用计数
总结:TBB 在任务调度和工作窃取上,尽量用无锁和原子操作,保证高性能并发执行。
b. 仍然存在锁的场景
队列扩容(grow)
某些并行容器
全局管理或 arena 初始化
| 特性 | 是否无锁 | 说明 |
|---|---|---|
| 工作窃取 deque(尾部 push/pop + 头部 steal) | ✅ 无锁(原子操作) | 性能关键部分 |
| 任务引用计数 | ✅ 原子操作 | 保证任务生命周期安全 |
| 并行算法内部调度 | ✅ 大部分无锁 | 依赖原子和无锁队列 |
| 队列扩容 | ⚠️ 使用轻量锁 | 极少发生,性能影响有限 |
| 并行容器 | ⚠️ 部分锁 | 保障线程安全,复杂容器需要 |
是的,即使你只调用一次 TBB 并行函数,线程池创建后在整个程序运行期间也不会自动销毁或减少线程数。具体说明如下:
TBB 默认全局线程池是在 第一次使用 TBB 并行接口时创建的。
线程数固定(通常等于 CPU 核心数),整个程序运行期间一直存在。
不会因为你长时间没有提交任务而销毁线程,也不会自动减少线程数。
长时间不使用 TBB 时,线程 处于空闲等待状态,不会占用 CPU。
内存占用仍然存在,因为线程堆栈和线程管理结构仍在。
一旦再次调用 TBB 并行函数,线程立即复用,无需重新创建。
如果希望线程在长时间不使用时释放资源,可以:
手动控制线程池生命周期(较复杂,需深入 TBB 内部 API,不推荐)
程序设计上在长时间空闲时退出进程,释放所有线程和内存
一般服务器程序会让线程池持续存在,利用 TBB 的工作窃取和线程复用优势,提高后续任务性能。
✅ 总结
TBB 线程池线程数固定,不会因长时间不使用而减少。
线程空闲时不占用 CPU,但占用内存和线程管理资源。
这种设计是为了 提高任务再次执行的响应速度,适合服务器或长期运行的高性能程序。
如果你的程序有大量 I/O 操作,使用 TBB 的一些特点和注意事项需要特别留意,因为 TBB 是为 CPU 密集型任务 和 任务并行化 设计的。具体分析如下:
结论:TBB 默认不适合大量阻塞 I/O 的场景
| 问题 | 原因 | 后果 |
|---|---|---|
| CPU 利用率低 | 阻塞 I/O 占用线程 | 任务等待,性能下降 |
| 线程不足 | 阻塞线程占用全局线程池 | 并行度降低,任务调度受阻 |
| 死锁风险 | TBB 线程池线程被长时间阻塞 | 如果任务依赖其他并行任务,可能互相等待 |
a. 分离 I/O 线程
不要在 TBB 线程池中直接做阻塞 I/O
可以开独立线程或线程池专门处理 I/O
TBB 线程只处理 CPU 密集型任务
b. 使用异步 I/O
对网络 / 文件 I/O 使用非阻塞或异步 API(如 asio、io_uring)
任务提交给 TBB 时 立即返回,I/O 完成通过回调或 future 处理
c. task_arena + 限制线程
如果必须在 TBB 线程里做少量阻塞 I/O,可以创建一个 局部 task_arena,限制线程数,避免阻塞全局线程池
d. 混合模型
CPU 密集任务用 TBB
阻塞 I/O 用专门线程池 / 异步框架
最后用 future/promise 或 task_group 协调结果
TBB 是 CPU 密集型并行框架,不适合大量阻塞 I/O
阻塞 I/O 会占用线程池线程,降低并行度
建议:
分离 I/O 线程或线程池
使用异步 I/O
TBB 只处理计算任务
gcc是一个编译套件,包含c、c++、Fortran语言的编译器。
glibc是一个library,为C程序提供基础公共功能,包括系统调用、数学函数和其他核心组件。
Linux平台和vscode似乎都依赖glibc,如果擅自将LD_LIBRARY_PATH更改为其他版本的glibc路径,则bash会直接crash。
1 | $ cd glibc-v2.34/Linux/RHEL7.0-2017-x86_64/bin && ls |
1 | # 从上可知,ldd是glibc的核心组件之一 |
1 | $ locate libc.so |
Ubuntu平台
1 | sudo apt-get install lib6 |
RedHat平台
1 | sudo yum install glibc |
1 | $ strings /usr/lib/libstdc++.so.* | grep LIBCXX |
如果你有一个使用了libstdc++的特定的binary或application,可以用下面的命令来检查其版本:
1 | $ ldd <your_binary_or_application> | grep libstdc++ |
使用vscode的“Remote SSH”工具试图连接到Linux时,可能会报错如下:
Warning: Missing GLIBCXX >= 3.4.25! from /usr/lib64/libstdc++.so.6.0.19
Warning: Missing GLIBC >= 2.28! from /usr/lib64/libc-2.17.so
Error: Missing required dependencies. Please refer to our FAQ https://aka.ms/vscode-remote/faq/old-linux for additional information.
这是因为Linux系统上的glibc版本中不包含GLIBCXX_3.4.25及以上的版本。此时需要降级vscode(建议做法)或升级glibc(似乎很难)。
你可以让某些程序默认使用你本地安装的 glibc(比如 ~/local/glibc-2.42),但不能全局替换系统默认 glibc,否则会导致系统崩溃。下面是几种安全又实用的方法,让你的程序“默认”使用本地 glibc:
✅ 方法一:使用 LD_LIBRARY_PATH 环境变量(推荐)
这是最简单的方式,让你运行的程序优先使用你指定的 glibc:
1 | export LD_LIBRARY_PATH=$HOME/local/glibc-2.42/lib:$LD_LIBRARY_PATH |
你可以把这行加到 ~/.bashrc 或 ~/.profile 中,这样每次登录都会自动生效。
📌 注意:这只影响你当前用户的 shell 环境,不会影响系统服务或其他用户。
✅ 方法二:使用 ld.so.preload(⚠️危险,不推荐)
你可以将本地 glibc 的路径写入 /etc/ld.so.preload,让系统优先加载它。但这会影响所有程序,包括 ls、bash、sudo 等,一旦路径错误,系统可能无法启动。
1 | echo "$HOME/local/glibc-2.42/lib/libc.so.6" | sudo tee /etc/ld.so.preload |
⚠️ 强烈不建议使用此方法,除非你在容器或 chroot 环境中测试。
✅ 方法三:使用 patchelf 修改程序的动态链接器
你可以用 patchelf 工具修改 ELF 程序的运行时 glibc 路径:
1 | patchelf --set-interpreter $HOME/local/glibc-2.42/lib/ld-2.42.so \ |
这样修改后的程序会自动使用你指定的 glibc,无需额外设置。
✅ 方法四:编译时指定 glibc 路径(适用于 CMake/GCC)
如果你自己编译程序,可以在编译时指定 glibc 的头文件和库路径:
1 | gcc -I$HOME/local/glibc-2.42/include \ |
或者在 CMake 中设置:
1 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wl,-rpath=$HOME/local/glibc-2.42/lib") |
🧠 总结:推荐做法
| 方法 | 是否推荐 | 说明 |
|---|---|---|
| LD_LIBRARY_PATH | ✅ 推荐 | 简单、安全、可控 |
| patchelf | ✅ 推荐 | 精确控制某个程序 |
| 编译时指定路径 | ✅ 推荐 | 适合开发者 |
| 修改 /etc/ld.so.preload | ❌ 危险 | 可能导致系统崩溃 |
/lib
/lib64
/usr/lib
/usr/lib64
/usr/include(头文件)
这些路径通常由系统预设,glibc 安装时会自动放置在这些位置。
链接阶段:GCC 会调用链接器(ld),查找 libc.so.6 或 libc.a 来完成符号解析。
C_INCLUDE_PATH:指定头文件搜索路径
LIBRARY_PATH:指定库文件搜索路径
LD_LIBRARY_PATH:运行时动态库搜索路径
例如:
bash
export C_INCLUDE_PATH=/opt/glibc/include
export LIBRARY_PATH=/opt/glibc/lib
4. 链接器参数
你也可以通过 GCC 的 -Wl 参数直接告诉链接器使用哪个 glibc:
bash
gcc hello.c -o hello
-Wl,–dynamic-linker=/opt/glibc/lib/ld-2.34.so
-L/opt/glibc/lib
这会指定使用 /opt/glibc/lib/libc.so.6 和对应的动态链接器。
bash
patchelf –set-interpreter /opt/glibc/lib/ld-2.34.so hello
patchelf –replace-needed libc.so.6 /opt/glibc/lib/libc.so.6 hello
这在部署多版本 glibc 时非常有用。
🧪 如何验证 glibc 的使用情况
查看程序依赖的 glibc:
bash
ldd ./hello
查看系统 glibc 版本:
bash
ldd