Effective STL 读后感

这本书的全名叫《Effective STL — 50条有效使用STL的经验》,书其实已经有点历史了,Meyers在2001年时给它写的前言,如今已经2022了。 正如上面所说,因为这本书已经有些年头了,如果你已经是C++11、14、17、20的使用者,可以把第6章函数子、函数子类、函数及其他的内容快速带过看,因为std::functional以及lambda函数已经让C++函数类支持有了显著提升。 简略写一下对我而言有用的一些经验: 第12条:切勿对STL容器的线程安全性有不切实际的依赖 多线程读是安全的 多线程对不同容器做写入是安全的 第14条:使用reserve来避免不必要的重新分配 尽管我认为这应该是C++ Engineer的常识了,不过还是有好多小朋友不知道这个点。 第23条:考虑用排序的vector代替关联容器 可以详细看一下。我觉得根本问题是map类的容器对cache不是很友好。连续内存访问总是很香的。 第25条:熟悉非标准的散列容器 写书的时候还没有stl::unordered_map。不过即便放2022年,我也觉得可以看看STL之外的散列容器实现,比如robin-hood-hashing。曾经用过比较坑的一个容器是boost::flat_map,千万别认为它是O(1)的实现。 第31条:了解各种与排序有关的选择 <algorithm>头文件里有很多宝藏,在选择自己实现之前,最好看看STL有没有帮你写好了轮子。 第32条:如果确实需要删除元素,则需要在remove这一类算法之后调用erase 曾经有个说法是std::move does not actually move anything, 对于std::remove也有点类似。 第44条:容器的成员函数优先于同名的算法 如题。 第47条:避免产生“直写型”(write-only)的代码 工程上非常中肯的一条经验。如果没有特殊原因(如及其苛刻的性能要求),尽量不要写出非常难懂的代码,不然几个月乃至几年之后你来调试自己写的代码时也会骂娘。 代码被阅读的次数远远大于它被编写的次数

C++ SFINAE 检测是否存在某个名称的成员函数

SFINAE(Substitution failure is not an error) 被用在很多模板的花式操作里,笔者使用的时候多半是为了将运行时的多态替换成编译器的静态多态。 简单版:检测一个类T是否存在某名字的成员函数 举个例子,有如下的类定义: 希望能有一个静态的检测,能够实现 那么可以这样[1]: 基本逻辑是如果被测试的typename T有成员函数,那么调用HasSomeWhatFunction的静态方法test<T>()时,模板替换是按照返回值类型为one(char)来生效的;反之如果不存在SomeWhatFunction的函数,那么返回类型one的模板替换失败,继续查找,匹配到万能重载(catch-all overload) 的static two test(…)里。 这种做法的可读性在模板的骚操作里算是好的,但是它无法区分SomeWhatFunction的函数签名(参数类型、返回值类型)。 普通版:检测一个类T是否存在某签名的成员函数 接上个例子拓展一下: 这个就需要更复杂一些的逻辑了: 可以看到这个实现比之前的多定义了一个内部类Check。在初始化enum value值时会调用test,然后会尝试模板替换,进而想要去特化struct Check。如果失败了(不存在对应签名的函数),那么会回滚到static two test里。 这个实现其实能够覆盖许多场景了,但是还有一个奇怪的需求满足不了:如果要判断一个类的模板成员函数呢? 地狱版:检测一个类T是否存在某签名的模板成员函数 小改一下SomeWhatClass 如果需要检测void SomeTempMemberFunc<int, T&>是否存在,咋办? 搜了一圈,解释的最清楚的是参考[2]里面描述的,利用std::declval的解法。 几个知识点: decltype里利用了含有逗号运算符[3]的表达式; declval可以避开构造函数而使用类成员; 参考 [1] writing and using a C++ template to check for a function’s existence http://www.cplusplus.com/forum/beginner/70134/ [2] SFINAE Hell: detecting… Continue reading C++ SFINAE 检测是否存在某个名称的成员函数

[C++] 在linux或windows上使用direct io

首先 Direct IO是一种不用内核缓存的IO, 它可以做到直接将用户空间的内存直接写入磁盘或者将磁盘数据直接读到用户空间的缓冲区,这种策略就是不用内核的缓存而使用用户自己设计的缓存. 需要注意的是,使用DirectIO会完全绕过系统的预取(prefetch)以及页缓存机制,如果不是必须,那么我认为还是优先考虑普通的read或者直接mmap吧。 Linux 几个方面注意一下就可以了 在调用[open]1时,把O_DIRECT加上。比如int fd = open("/path/to/file", O_DIRECT, O_RDONLY); 用于文件读写的buffer,必须和磁盘的块大小对齐(保守起见一般可以设为4KB)。有两种方法能拿到地址对齐的内存块: 直接使用posix_memalign; 直接new一段内存,然后根据返回的内存地址,往后找到第一个满足对齐要求的地址就可以。这种方法会浪费前面一段空间,不过其实posix_memalign在系统操作的时候”浪费”了; 借用mmap申请MAP_ANONYMOUS匿名映射,addr参数填NULL的话mmap出来的地址是页对齐的(至少是4K对齐),所以可以直接拿来用; 如果需要lseek之类的操作,注意seek的文件位置偏移量必须是磁盘块大小的整数倍; 2.4内核下, 每次文件读写的长度必须是块大小的整数倍(e.g. N * 4KB). Linux 2.6.0+无此要求; Windows下 参考这篇文章,可以知道Windows下也可以启用类似的机制,对应的打开文件flag是FILE_FLAG_NO_BUFFERING.用法类似,与Linux不同的地方在于: 在CreateFileA(…)调用的dwFlagsAndAttributes参数里把FILE_FLAG_NO_BUFFERING填上; 用于文件读写的buffer也是需要对齐的 也可以使用类似的方法_aligned_malloc申请对齐的内存,但是必须注意要使用_aligned_free释放内存,否则runtime error;

Boost Python的C++对象, Pickle支持及其原理

默认用boost python包裹的C++对象是不支持pickle的,如果要用pickle.dumps(obj)的话那会提示错误 Pickling of "xxx" instances is not enabled. 这边吐槽一下在最新的代码里,给的reference链接其实还是不可用的。真正正确的是https://www.boost.org/doc/libs/1_74_0/libs/python/doc/html/reference/topics/pickle_support.html。 让你的class支持Pickle协议 若要让你的C++ Class支持Pickle协议,比较“正统”的方法是利用boost提供的boost::python::pickle_suite. 拿代码说话: struct world_t { world_t(const string& country) { … } }; struct world_pickle_suite : boost::python::pickle_suite { static boost::python::tuple getinitargs(const world_t& w) { // [可选实现] 返回一个boost::python::tuple元组,其值被用来构造 // 如果一个类的构造函数**不需要参数**的话,可以不用重载这个方法。 return boost::python::make_tuple(w.country()); } static boost::python::tuple getstate(const world_t& w) { // [可选实现] 如果对象的构造函数并不能完全恢复对象的状态, // 那么要用此函数返回其状态值 }… Continue reading Boost Python的C++对象, Pickle支持及其原理

记录一下找了半天的huge page坑——fork越来越慢的原因

背景 之前发现Jupyter Notebook下面,如果数据占用多的话,开多进程池会特别的慢。一开始以为是Python的锅,但是把multiprocessing.pool改成直接用os.fork()调用以后,问题依旧。照理来说unix下面使用fork开进程,会启用copy-on-write机制,内存增长并不是特别明显,但是实际在htop下面看内存仍然会在fork之后增长,并且和进程数量是线性相关的。 原因 随后想了老半天,想到了可能和页表有关系。查了一下,跑的服务器上huge page确实被禁用了(不知为何…). fork的机制简单地说,是在创建新进程的时候把老的进程控制块(Process Control Block)里内存页表拷贝给了新的PCB——这边具体内存的信息是不拷贝的。由于当时Notebook跑的数据处理任务,里面已经用了不少内存(100GB+),所以拷贝的时候如果用默认的4KB内存页,将会有100 * 1024 * 1024 / 4 = 104,857,600个页表! 按典型一个页表项(Page Table Entry)大小4Bytes计算,一个进程开出来光页表会耗400MB内存.

Support parallel XZ decompression for unix (7zip LZMA SDK based, C/C++)

为unix平台增加XZ多线程解压缩支持(基于7zip LZMA SDK, C/C++) Note This post has nothing to do with the pixz project. I am talking about decompressing the original xz archive using 7-zip’s LZMA SDK under unix environment. Background Originally the 7zip’s LZMA SDK (version 19.00) only covers parallel xz decompression for Windows systems. This post shows the C code that adds… Continue reading Support parallel XZ decompression for unix (7zip LZMA SDK based, C/C++)

std::future 与 std::promise 简单使用、简单原理

C++11有了一些关于线程的模型,在此之前C++里可是各自为政的,各种线程库各种神奇用法。其中有两个好玩的东西就是std::promise<T>和std::future<T>,下文书中形容它们像是个“虫洞”。 std::future是虫洞的出口:一个未来对象的“获取器”,在未来的某一刻它能返回一个有用的值,但现在还没有… std::promise是虫洞的入口:我们要保证在未来的某一刻给一个值进去,但不是现在… Basic usage 一般来说这俩是成对使用的,看个代码: #include <cassert> #include <chrono> #include <future> #include <iostream> #include <string> #include <thread> using namespace std::chrono_literals; int main() { std::promise<int> p1, p2; std::future<int> f1 = p1.get_future(); std::future<int> f2 = p2.get_future(); p1.set_value(42); assert(f1.get() == 42); std::thread t([&]() { std::this_thread::sleep_for(100ms); p2.set_value(43); }); auto start_time = std::chrono::system_clock::now(); assert(f2.get() == 43); std::chrono::duration<double, std::milli> elapsed… Continue reading std::future 与 std::promise 简单使用、简单原理

C++17 std::variant 简单使用

std::variant应当是用来替代union来使用的,后者从C时代就有了,但是它缺乏类型安全的保障。boost库很早之前就有了实现(其实从C++11开始,感觉boost不知道多少东西被借鉴走了)。std::variant的类长这个样子: template <class… Types> class variant; Usage 使用起来也是比较方便的 std::variant<int, double> v1; v1 = 1; // activate the "int" member assert(v1.index() == 0); assert(std::get<0>(v1) == 1); v1 = 3.14; // activate the "double" member assert(v1.index() == 1); assert(std::get<1>(v1) == 3.14); assert(std::get<double>(v1) == 3.14); assert(std::holds_alternative<int>(v1) == false); assert(std::holds_alternative<double>(v1) == true); assert(std::get_if<int>(&v1) == nullptr); assert(*std::get_if<double>(&v1) == 3.14); 上面代码里的几个好用的constexpr函数:… Continue reading C++17 std::variant 简单使用

C++17 std::generate的使用

std::generate是“遍历——执行”工具函数的一种 #include <algorithm> #include <iostream> #include <string> #include <vector> int main() { std::vector<std::string> v(4); std::generate(v.begin(), v.end(), [i=0]() mutable { return ++i % 2 ? "hello" : "world"; }); for (auto&& s : v) { std::cout << s << " "; } return 0; } 上面的函数输出是hello world hello world . std::generate(beginIt, endIt, generator)函数接受起止的迭代器以及一个生成方法。

C++17 新特性—— if constexpr

看代码说话 template <typename Iterator> auto distance(Iterator begin, Iterator end) { using Traits = std::iterator_traits<Iterator>; if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename Traits::iterator_category>) { return end – begin; } else { auto result = typename Traits::difference_type(); for (auto it = begin; it != end; ++it) { ++result; } return result; } } 上面的代码用来判断两个iterator之间的距离。主要逻辑是判断iterator是否是可随机访问的iterator然后作分支处理,可以随机访问的迭代器直接把iterator相减。在constexpr if语句中,条件的值必须是可按语境转换到 bool 类型的经转换常量表达式。若其值为 true,则舍弃 false分支语句(若存在),否则舍弃 true分支语句。有意思的一点是,被舍弃语句中的… Continue reading C++17 新特性—— if constexpr