转行机器学习一周年 & 从业十余年杂谈

2023 年 5 月,我已经在川大 KDE Lab 渡过了近一年浑浑噩噩的硕士研究生时光。这时,刚从美国归来的唐老师给我的学术生涯带来了一丝转机,从此我便追随唐老师,走上了 ML(Machine Learning,机器学习) 研究的不归路。接下来的一年里,我完成了数次顶会、顶刊投稿。虽然目前还没中,但明显感觉到已经摸到了科研的技巧,明显比大四和研一的时候好很多了。这些事情还要一点点往回说。研究生选择 KDE Lab 是因为段老师待我不薄,从 2018 年进入川大以来一直都很照顾我。既然对未来的研究方向非常迷茫,不如选择一个自己熟悉的老师。但是从大四进入实验室以来,经过一年多的科研训练,依旧还是很难对知识图谱相关的研究方向产生兴趣。

我曾经是一名野生程序员,虽然从小学开始就已经在父亲的影响下开始学习 Visual Basic 等非常基础的编程语言,但真正的从业生涯要追溯到 2013 年初,也就是我初一的时候。那时候,我在学校的科技展上看到了 C++ 编程语言,觉得很酷,就从书店购买了一本《Accelerated C++》自学入门。没想到这个选择影响了我十年之久——后面的十年里,我与编程语言、底层系统结下了不解之缘,于 2017 年在 C++ 等底层语言思想的影响下设计并编写了 Covariant Script 编程语言,并为其编写了完善、独立的工具链。这件事情帮助我通过自主招生进入了四川大学,这是我当初在设计 CovScript 时未曾设想过的。这件事同样帮助我开启了硕士研究生的生涯——因为我是以“双特生”身份进入的四川大学,双特生只需通过吴玉章学院每年一度的专家考核即可获得保研资格,不需要通过绩点、比赛等方式与大多数普通同学竞争。这虽然事实上并没有降低多少难度(每年考核我都发怵),却帮助我能更加专注于专业领域的研究。作为结果,本科毕业时我交出了基于 CovScript 编写的高效实时 LR Parser Generator 作为答卷,我自己对于这个成果非常骄傲,因为这不仅证明了我自己在 Paring 算法方面的深刻理解,也证明了我设计的编程语言足以完成这种复杂的工作。也就是说,经过大学本科四年的努力,我顺利完成了野路子到科班的转变。

看了这些,聪明的读者肯定明白了我为什么进入硕士研究生会感到非常不适应:我将近十年以来的技术栈可以说和知识图谱没半点儿关系啊!但为什么同样都是没关系,为啥我转行做 ML 却很顺利呢?答案是:都不顺利。大家都知道,ML 领域的两大框架,TensorFlow 和 PyTorch,都是以 Python 为载体的。但我一直在和同学、同事、朋友们说,在 2023 年 5 月之前我连 Python 都不会写,这一点都不是开玩笑和凡尔赛,我学习 Python 和 PyTorch 的过程也是用不少头发换来的(多亏我爸妈送我一头浓密的头发,年轻时掉一些不要紧)。

聪明的你肯定又要问了,为什么我做 ML 能做的动呢?这就不得不提到以 ChatGPT 为首的生成式人工智能(AGI,Artificial Generative Intelligence)为 ML 领域带来的巨大的革新了。在此之前,ML 解决实际问题依赖于为特定领域设计非通用型的神经网络,大模型(LLM,Large Language Models)的出现让全世界看到了通用人工智能的可能性。但大模型有个很致命的问题就是太耗资源了,无论是预训练(Pre-training)、微调(SFT,Supervised Fine-Tuning)、偏好对齐(RLHF,Reinforce Learning by Human Feedback),还是推理(Inference),都需要算力集群才撑的起来。现在(2024 年 9 月)来看这些都不是很大的问题了,无论是小模型的发展还是模型蒸馏(Distillation)、剪枝(Pruning)和量化(Quantization)等小型化技术,都较好的解决了大模型消耗资源的问题。但在 2023 年 5 月,这些问题都没得到很好的解决,因此我看到了在软件基础设施(Infrastructure)和参数高效微调(PEFT,Parameter Efficient Fine-Tuning)方面的巨大机遇。你说巧不巧,到今年正好碰上了小模型(SLM,Small Language Models)的爆发,以 Gemma、Phi 为首的小模型开始逐渐在细分领域替代传统大模型。

所以啊,我给我自己总结的性格特点是,如果有一样东西没有实际的意义,我根本学不进去。这就是为什么一直以来我数学都不太好,因为数学里大多数概念都太抽象了。说到数学,我不得不想到从高中开始就被各种老师谆谆教导,计算机里面数学很重要、数学好好学,但直到我自己开始做 ML 方向的科研之后,才真正体会到数学的重要性。曾经的我 Naive 地以为人工智能/机器学习净是一堆炼丹炼出来的概率模型。这句话一半对,一半错。对是对在机器学习确实是基于概率模型,它并不是一种通过传统编码方式产生的 Deterministic Turing Machine(确定性图灵机,实际上这也只是理想状态);错是错在机器学习的炼丹和规则都是有据可寻的,包括最常见的各类正则化方法、激活函数、梯度下降、交叉熵等,均建立在扎实的数学推导上。这其实是挺让我惊讶的,实际上是打了我前几年宣称的言论——“数学不重要”的脸,充分证明了人都是会变的(误)随着学识的增长,会逐渐消除年轻时对绝大多数事物的偏见

上一次写类似的博客是在刚考上四川大学的时候,其实有一段时间没有类似的心情和感想写博客了,主要是因为自己来到成都后的六年以来发生了太多事情,我自己的见识、性格和经历也以年为单位发生了数次巨大的转变。我自己非常庆幸能遇到包括高老师(初三)、英老师(高一)、韩老师(高二~高三)、段老师(本科~研一)和唐老师(研二~至今)在内的贵人,没有他们就没有我今天的成绩。但至少最近一次赶上大模型发展的快车最应该要感谢的是我自己,我自己下定决心从零开始,花了大概半年时间摸清了 Python、Transformer 等基本功,并几乎是献祭了自己在算法方向的第一个成果 MixLoRA(2023 年 12 月底有初步算法,2023 年 1 月中旬完成了可行性验证,但因为过年和实验进展问题错过了年初的两次投稿机会,丧失了先发优势,最终直到 2024 年 4 月才在 arXiv 挂出了第一版论文),才敢说自己的科研能力上路了。

我其实深知自己不是什么天才,顶多算是有一些小聪明的普通人,和那群蹲在电脑前头几个月就能憋出来一篇顶会的人比简直就是学术垃圾。但不比不知道,一比吓一跳啊。今年 6 月实验室走了一批元老级的本科生,人均一篇 A 类顶会/顶刊文章带去美国读博了,和这群神仙比我自然是小趴菜。但同时实验室也来了一批新的实习本科生,带他们就明显感觉力不从心。这时候回过头来审视自己:为什么我能在半年之内入门 ML 科研呢?我怎么做才能在下次技术浪潮来临时同样走在前面,避免被后浪拍在沙滩上呢?

我常常感到迷茫,这种迷茫与大多数人眼中外露的我截然不同。高中时,我便有了这种困惑,感觉自己的兴趣和追求与同龄人相去甚远,仿佛与世界格格不入;大学期间,虽然胸怀志向,但四处碰壁,找不到前行的方向;硕士阶段,抬头望去,天外有天,深感渺小,低头俯视,却发现脚下的世界无边无际。成年后,经历了大大小小的挫折与变故,我愈发感受到个人的渺小与生命的短暂。每当我坐在灯火通明的饭局上,觥筹交错,听着那些繁复的恭维之词时,不禁觉得所谓的人际资源和社会分配,不过如此而已。我常常思索,或许,多陪伴家人和爱人,这一生便无憾,又何必去追寻那些看似遥不可及的目标呢?

但我的内心坚定不移地告诉我:不,我必须追求。从世俗的角度来看,只有追求更高的目标,才能为自己和家人带来更富足的物质生活;而从自我实现的角度出发,只有不断向更高处迈进,才能为自己的人生勾勒出一条鲜明而充实的主线,让这个世界记住我这个“小人物”曾经存在过的痕迹。于是,前面我所提出的问题便有了明确的答案:正因为我追求自我实现,所以我才是一个高度自驱、自律,并对未来有明确规划的人(尽管方向偶尔会调整,但总体目标始终不变)。正是这种追求,让我从初中开始逐渐培养出了超越同龄人的自学能力和信息检索能力。这种能力不仅支撑我在半年内顺利入门 ML 科研,也将成为我在未来技术浪潮中始终走在前列的力量源泉。

或许,正如韩老师在我高中毕业时的留言所说:“世界会向那些有目标和远见的人让路。”希望我能够坚持自我,在接下来的最后一年硕士生涯中,顺利摘得学术界的桂冠——取得顶会或顶刊的成果。同时,在即将到来的职业生涯中,也能拼搏出属于自己的一片天地。

——2024 年 9 月,于成都家中。

CovScript CNI 杂谈

随着 CovScript 的逐渐发展,其生态目前也已经基本成型,有了作为一门生产级编程语言的潜力。但也有一些朋友在完成一些需求时需要动态加载某些功能,CNI 在这个时候就显得非常有用,因此我决定将 CNI 独立成一个模块,供有特殊需求的朋友使用。

CNI Standalone Edition

CovScript CNI SE:https://github.com/covscript/cni

SE 的意思是 Standalone Edition(独立版),去除了庞大的 CovScript Runtime 和 Interpreter 相关代码,编译出的静态库不到 100k 大小

CovScript SE 包含了两部分功能,分别是 CNI 本身以及 CovScript 扩展系统和 CNI 组成宏。下面分别介绍这三者以及它们之间的关系。

CNI 在最初是独立于 CovScript 的一个部分,其主要作用是通过 C++ 模版元编程相关的技巧在编译期解析一个 C++ 函数的各种信息,比如返回类型、形参列表等,再根据这些信息将这个函数包装为一个 CovScript Callable 对象,其本质是一个返回值为 cs::var,参数为 cs::vector => std::vector<cs::var>。也就是说,CovScript Callable 对象和很多动态语言的 Foreign Function 很相似,只不过从指针满天飞的形式变成相对安全、易读的方式(cs::var 能保证类型安全和资源安全)。

实际上,利用这个特性,CNI 能够用于实现类型擦除,从而能够将各式各样的函数以类型安全、资源安全的形式传递到各处。CNI 本身是个 C++ Callable Class(重载了 operator() 的类),其构造函数能接受任意形式的 C++ 函数。利用这个特性,可以编写如下的代码:

#include <covscript/covscript.hpp>

cs::cni func(foo1);
cs::vector args{1, 2, 3};
func(args);

至于 CovScript 扩展系统和 CNI 组成宏,可以参考这篇博客:https://unicov.cn/2019/05/16/cni-composer-macro/

总的来说,CNI 用于实现类型安全、资源安全的类型擦除,将任意 C++ 函数转换成 CovScript 能识别的类型;CovScript 扩展系统是 CovScript Runtime 能够动态加载其他功能的关键,基于动态链接库实现;CNI 组成宏则是在前两者基础上的一种 API 无关的标准,能用简单易用的方式编写 CovScript 扩展。

目前来说,CovScript 生态中大多数扩展都是基于上面三个系统构建的。也就是说,CovScript CNI SE 能兼容大多数 CovScript 现有扩展,只需要使用 CovScript CNI SE 重新编译就能很方便的引入到自己的应用中,将 CovScript 的能力转换为自己的能力。反之亦然,使用 CovScript CNI SE 打造的扩展也能以极低的成本移植到 CovScript 生态中。

使用 CNI SE

那么就只剩一个问题,如何使用 CovScript CNI SE 加载扩展呢?

下面是一个示例程序:

#include <covscript/covscript.hpp>

int main(int argc, const char **args)
{
    if (argc != 2)
    return -1;
    cs::extension dll(args[1]);
    cs::function_invoker<void(std::string)>
        func1(dll.get_var("print"));
    func1("Hello");
    cs::var func2 = dll.get_var("print");
    cs::invoke(func2, "Hello");
    return 0;
}

在这段程序中展示了两种调用扩展中函数的方式。但在调用之前是将变量提取出来,方法也非常简单,即 dll.get_var,关于这个函数的详细注解如下:

  • dll 是一个 cs::extension 对象,在构造时传入路径,在失败时抛出 cs::runtime_error 异常
  • get_var 方法接受一个 std::string ID,返回 cs::var,在 ID 不存在时抛出 cs::runtime_error 异常
  • cs::runtime_error 异常是 std::exception 的子类

获取到扩展中的函数 func 后,分别可以用 cs::function_invoker 或 cs::invoke 调用。

cs::function_invoker 是一个模版类,需要手动传入目标函数的返回类型和形参列表,能够将一个 cs::var 包装为适合在 C++ 中使用的强类型对象

cs::invoke 是一个模版函数,可以动态的调用 cs::var 形式的 cs::callable 对象,返回 cs::var

一般来说,cs::invoke 更常用,但可能存在 C++ 中类型和 CovScript 中类型不能完全兼容的问题;cs::function_invoker 是 CovScript SDK 提供的基础设施,能对不兼容的函数进行基础的转换,兼容性更好

若要对 cs::var 进行手动操作,可以直接赋值,提取其中的变量可使用 val<T>() 和 const_val<T>() 两种形式。当类型不同或权限错误(比如使用 val<T>() 提取受保护的变量)都会抛出 cov::error 异常

编译 & 兼容性

上面提到,CNI SE 仅是代码级兼容,本质上还是需要重新编译,不算完全兼容。这是因为 CovScript SDK 中有很多 CNI 之外的功能,CNI SE 为了轻量化将这部分全部剥离了,而这部分功能又是 CovScript Runtime 必须的功能,因此没办法做到两者的互相兼容。

那读者可能就要问了,如果想直接二进制兼容呢?

其实这个问题的答案已经浮现在各位眼前了:直接使用 CovScript SDK。

CovScript SDK 和 CovScript CNI SE 形式是一样的,都是通过头文件 + 静态库的方式分发。不同的是 CovScript SDK 不能直接添加为 CMake 的子文件夹来引入到项目中,只能随 CovScript Runtime 一同下载 & 编译,原因是 CovScript SDK 的依赖更复杂,没办法直接通过 CMake 编译。

使用 CovScript SDK 最简单的方式是从官网(https://covscript.org.cn/)上下载,然后将运行时环境所在的位置(Windows:C:\\Program Files (x86)\\CovScript,macOS:/Applications/CovScript.app/Contents/MacOS/covscript,Linux 无需单独设置)设置为环境变量 CS_DEV_PATH,并在 CMakeLists.txt 头部加入以下内容:

if (DEFINED ENV{CS_DEV_PATH})
    include_directories($ENV{CS_DEV_PATH}/include)
    link_directories($ENV{CS_DEV_PATH}/lib)
endif ()

最后再将目标链接至 libcovscript 即可,使用 CNI SE 时也是一样。

至于 CNI SE 本身的编译,可以参考 CNI SE 的 GitHub Repo 中的介绍,可以使用 csbuild 脚本编译,也可以通过将 CNI SE 设置为 CMake 子文件夹来直接引用。

CovScript 4来啦!从3.4版本开始,ECS将随运行时一同发行

经过了近两年的Preview后,CovScript 4(Extended Covariant Script, ECS)终于逐渐成体系了。目前Cov4主要是由三部分组成,分别为编译器(ecs)、语法扩展(*.ecsx)和支持库(ecs, ecs_parser, ecs_generator, ecs_bootstrap)。事实上,整个CovScript 4的生态都构建在CovScript 3的基础上。

由于CovScript 4并不是刚需,因此为了降低运行时安装包的体积,我们采用了在运行时中附带一个简单的ECS启动器,这个启动器将扫描当前环境中是否存在ECS Bootstrap环境,只有通过CSPKG安装了这个环境才会启动真正的ECS编译器。

安装ECS Bootstrap环境的方法也很简单,只需要在命令行中运行:

> cspkg install ecs_bootstrap --yes

通过CSPKG部署ECS的好处有很多。除了上面提到的原因外,由于ECS目前依旧处于Preview状态,通过CSPKG部署还能大大降低用户更新的复杂度,当我们发布补丁时,用户只需要在命令行中运行下面命令即可更新本地的支持库:

> cspkg upgrade --yes

希望大家喜欢~

关于Modern C++中Universal Reference的一些理解和谬误

Universal Reference只是一种技巧,并不是一种C++中内建的语法糖。实际上所谓Universal Reference本身只是模板推导、引用折叠以及类型退化共同构成的一种编程范式。

通用引用是否就是形如T&&的引用?

不是。通用引用成立的非常重要的条件之一就是模板类型推导。T必须是一个独立的、构成推导条件的模板类型参数。比如:

通用引用正确示例

template<typename T>
void uniref(T &&dat) {
// TODO
}

通用引用错误示例

template<typename T>
struct test {
void uniref(T &&dat) {
// TODO
}
};

通用引用的推导规则是怎样的?

首先需要介绍的概念是引用折叠。引用折叠本身也是存在于模板类型推导的过程中

引用折叠仅存在于使用通用引用承载左值时。大家都知道左值引用的形式为T&,若套用在通用引用的形式中,就会变成(T&)&&,这时外侧的&&会被自动忽略掉。这个行为被称为引用折叠。

其次是类型退化。退化的具体规则比较复杂,我们在这里仅介绍在通用引用中的类型退化。事实上之所以推荐在传递参数时使用通用引用,就是因为通用引用中发生的类型退化几乎是所有传参方式中最小的。

当通用引用承载右值时,注意这里是右值而不是右值引用,T&&会退化为平凡参数,或者说是退化为传值(Pass by value)。

最后是最简单的类型匹配。对于本身就是右值引用的参数,这时发生的是最简单的类型匹配。如传入int&&,通用引用的T&&将构成对这个类型的全匹配,因此不会发生任何多余的修饰。

也就是说,通用引用的推导规则可总结为下表:

参数左值左值引用右值右值引用
修饰折叠折叠退化全匹配
T&&T&T&TT&&

常见谬误

通用引用能保留参数的原有类型

并不能,具体可见上表中的推导规则。通用引用常用于实现完美转发,而完美转发依赖std::forward对类型进行还原。

若不进行还原,对于传入的右值很容易被判定为左值。这是传值带来的附加效果。事实上这里在函数返回之前的确是一个左值。

右值引用就是通用引用

长得像罢了

更多问题,可以在评论中回复。

怎样学习计算机程序设计

最近很多同学在协会跟着我们学习C++编程语言,遇到了一些问题

首先我们需要确定的几个事实:

  1. C++难不难?难!不然也不会有这么多人转向Java等语言,但这并不代表大家学不会C++。以本文的大多数读者四川大学学生为例,能考上985重点本科院校的同学,本身智商和数理基础是绝对没问题的
  2. 为什么学校的C++这么简单?因为学校教的内容不仅严重落后于工业界,其内容的深度和广度都非常欠缺
  3. C++是不是已经开始被淘汰?不是!C++只是随着计算机科学的发展逐渐收缩至其更专精的范围内
  4. C++更新标准(17/20)有没有必要?没必要!最新的标准至今还没有出正式提案,C++17则依旧属于Modern C++的范畴
  5. 以后工作中C++能不能用的到?当然能!目前C++就业主要是走系统软件和服务器后端,本科打好基础非常重要

这里必须强调的是,我们的课程难度是高于计算机专业课程的,不是普及计算机知识的科普课程。但这并不意味着你学不会,除非你连初中生都不如

那么问题出在哪?

学习方式有问题。

我这里就不想老调重弹应试教育害死人之类的,但我这里必须再次强调的是,无论你以前多牛逼,无论是算法打了多少奖,或是高考考了多少分,只要你写不出来我们要求的程序,你就是弟弟!扔掉以前的坏习惯,走出舒适区!

首先的要求就是要把Modern C++当作一门新的语言来学习,忘记你之前的所有好习惯坏习惯,千万不要看目录某个概念很熟悉就跳!过!去!了!

其次就是养成自己解决问题的习惯。有问题怎么办?扒搜索引擎!泡论坛!问同学!别动不动就找老李私聊,老李也有自己的生活,老李很忙!

再就是大多数同学都会遇到的问题,就是觉得没有头绪,觉得特别难,针对这个问题,我需要特别强调:

  1. 不要打破沙锅问到底!计算机这玩意本身就是抽象的,在你的思维成长到能够理解所有层面抽象之前,死记住就行了,千万别揪着一个问题不放
  2. 如果你遇到了一个困难,应当庆幸!因为如果一个人一直做觉得很简单的事情,那是不可能进步的。遇到困难,说明突破困难就能进步了
  3. 再巨的大佬也是从小白成长起来的,心态远比能力更重要

如果对一个问题实在是没有头绪,怎么办?

  1. 找一个列表,列出来自己需要实现的功能有哪些特点?尝试归纳总结
  2. 不要一口吃个大胖子,尝试先实现这个问题中最简单的部分
  3. 看看别人怎么做的?GitHub是个非常有用的示例程序库

最重要的,千万不要惧怕犯错。希望大家能仔细品味为什么是Learn by doing,这里面这个doing到底是在做什么。

我是怎么到今天的?一个词,头铁。实际上我做的事情在很多人看来是“不够聪明“的,我在刚开始学程序的时候就是头铁,啥都不会直接啃C++。很多人就说我傻,为什么不学Python,不学Visual Basic,不是更简单吗?但如今这个傻子自己写出了一门和Python能比一比的语言,而那群曾经嘲笑我的人可能还只是会写点Python小程序吧。

对比我初学程序时的环境,请大家不要再抱怨,自己多花点精力,挑战一下自己的极限。实在有问题想不清楚,还有我们在背后支持,你们已经比我们初学时要幸福一百倍了,千万不要身在福中不知福。

好耶!是小对象优化

论静态优化在C++程序设计中的应用

说到小对象优化,今年开始跟着我上课的朋友可能有一点印象,这是因为我在课程计划中提到了如何实现小对象优化,但后来由于课程进度等原因反正就是没讲,今天就说一下什么是小对象优化

自计算机诞生以来静态和动态就是非常矛盾的一对存在,静态的东西速度快,动态的东西功能强,因此静态化就是程序优化中非常重要的一个方面。今天我就打算利用任意容器(any)来讨论一下怎样给动态的东西进行静态优化

任意容器的原理和实现

任意容器在C++17标准中被纳入STL的范畴,这在一定程度上也凸显了任意容器这一原理简单且实用的工具的重要性

任意容器的原理说起来也非常简单,即类型擦除(Type Erasing),利用面向对象编程中的抽象方法将具体的类型信息全部“擦除”,具体来说就是将所有的类型全部抽象为一个Object

但这个时候就引出一个问题:为了达到这种效果,派生类对象往往会在堆上分配

在堆上分配就带来了两个显著的问题:

  • 首先是频繁分配带来的内存碎片问题
  • 其次是堆内存分配本身就是低性能的

其实这里内存碎片带来的主要问题其实还是性能,也就是说我们现在还是最关心一个问题就是性能

堆内存分配的性能优化

一般来说常用的堆内存分配性能优化手段有以下几种:

  1. 内存池,在大型项目中比较常见,也就是说自己维护内存的分配
  2. 缓冲池,预缓存一部分内存提供给IO热点,在CovScript 3的Any中得到了应用
  3. 缓式求值(Lazy Evaluation),利用写时复制(Copy On Write)等手段减少不必要的内存分配

这几种优化手段的优点显而易见,但也都有其缺点:

  1. 内存池往往对分配算法要求较高,且一次性会占用过多系统内存
  2. IO热点难以判断,可能会在IO不频繁的场景下反过来拖累性能
  3. 缓式求值传统上使用引用计数器和句柄实现,引用计数器会造成额外的性能开销,而句柄实现的间接内存访问也会破坏编译器的内联优化,在Modern C++中移动语义在一定程度上替代了缓式求值的地位

所以,最好的性能优化方案,其实是避免堆内存分配。

老李又开始说废话了,如果能用栈内存,谁想用堆内存啊!!!

曲线救国之小对象优化:引论

既然堆内存不好玩,为什么不搞点栈内存玩呢?

问题:能不能在栈内存上搞动态内存分配?

在这里请读者回顾一下C++中Placement New的相关语法

也就是说,C++可以在一块特定的内存上构造任意对象,这块内存既然可以是堆内存,当然也可以是栈内存,那么我们只需要拿到一块堆内存就可以了

但说到这里很多朋友就要开始挠头了,获取栈内存可以用malloc,堆内存用什么?

答案是数组

用数组的原因显而易见,单个变量所占用的堆内存往往大小有限

在这里我们常使用unsigned char作为数组的基本类型单位,因为unsigned char是C++中占用内存最小的内建类型,也就是八个字节,因此我们可以用unsigned char*合法的表示任何C++中的内存地址。

现在我们拿到了栈内存,也知道怎么往栈内存里面塞东西,那么接下来呢?

曲线救国之小对象优化:时间与空间的权衡

栈内存看起来好哇!那为什么大家不把整个程序的数据都存储在栈内存里面呢?

首先,栈内存一旦分配,大小是不能改变的(此处不考虑VLA变长数组)

其次,很多情况下内存的拷贝性能是比较差的,因此要考虑栈内存对对象大小的影响

再说深一点,过大的对象会导致Cache Miss,这样反而会得不偿失

什么是Cache Miss?

现代CPU往往通过多级缓存的方式提高内存访问的性能,然而缓存的大小是十分有限的,因此过大的对象可能导致CPU无法通过缓存加速其访问,因此会一定程度上影响性能

到这里我们就需要考虑一个恒久的话题了:怎样权衡时间和空间

较大的对象我们只能扔到堆里,从而避免浪费宝贵的栈空间

而较小的对象,我们就可以将其存储在栈里

但这个阈值怎么样来确定呢?是不是一股脑把所有的数据全部存储在栈上就能达到最佳性能?

过小的阈值是不是没有实际意义?毕竟如果装不了什么东西,到最后还是要扔到堆上

过大的阈值会不会浪费内存空间?毕竟大多数情况下大家都会选择将较大的内存块扔到堆里面

根据大量的测试,我发现3*sizeof(void*)是最合适的大小,巧合的是STL也选择了这个阈值

这个结果是综合了内存对齐和平均情况下内存的占用情况而定的,在实际使用中读者可以尝试自己调整一下阈值看看效果

曲线救国之小对象优化:实现

传统情况下any只需存储一个Object基类指针即可,引入小对象优化就带来了很多问题

  1. 存储小对象的Buffer应该放在哪里?
  2. 数据回收时如何处理不同的回收语义?
  3. 小对象复制时能不能直接按位复制?
  4. 怎样处理移动语义?

这里让我们一一进行讨论

使用CNI组成宏编写CovScript扩展

概念介绍

CNI是什么?

CNI,全称为C++ Native Interface,是Covariant Script与C++之间进行交互的抽象接口,几乎支持所有能被调用的东西

CNI组成宏是什么?

CNI组成宏是CNI标准扩展的一部分,旨在用简单易用的宏定义代替繁杂的 CNI 声明

CNI标准扩展又是什么?

C/C++ Native Interface Standard Extension,即CNI标准扩展,是Covariant Script解释器扩展的一部分,旨在降低中低复杂度的Covariant Script语言扩展的编写难度

让我们开始吧

必需头文件

#include <covscript/dll.hpp>

Covariant Script扩展头文件

#include <covscript/cni.hpp>

CNI标准扩展头文件

CNI根名称空间(CNI Root Namespace)

在CNI标准扩展中,我们首先需要接触到的第一个概念就是CNI根名称空间

Covariant Script在引入扩展时,会将扩展中的内容映射为Covariant Script语言中的名称空间,因此CNI根名称空间就是这种映射在C++中的表现

事实上,由于需要建立C++与Covariant Script扩展系统之间的联系,CNI根名称空间会自动完成一些必需工作。因此,我们几乎所有的操作,都必须在CNI根名称空间中进行

声明CNI根名称空间

CNI_ROOT_NAMESPACE {
    // C++代码
}

类似于C++的名称空间声明,在关键的CNI_ROOT_NAMESPACE宏后只需紧跟花括号组成的语句块即可

CNI根名称空间对应着C++中的名称空间名cni_root_namespace

CNI函数(CNI Function)

CNI函数名字叫做函数,实际上是一种C++函数到Covariant Script函数的映射

CNI函数是CNI组成宏的一部分,可以说是整个CNI标准扩展中唯一一个真正和CNI产生联系的部分,其他部分只能说是在配合CNI的工作

CNI系统是Covariant Script扩展系统的多个子系统之一,其功能为构建CNI抽象层以完成Covariant Script函数到C++函数调用的转换工作,具体原理我们这里就不再深究

声明CNI函数

// 声明C++函数
CNI(函数名)

我们只需要先在当前名称空间(必须是CNI名称空间)中声明C++函数,然后再在其后追加一句CNI组成宏,即可完成CNI函数的声明

Hello,world!

到这里我们已经介绍了最基础的CNI组成宏用法,下面就是喜闻乐见的实践环节了

示例代码

// CovScript头文件
#include <covscript/dll.hpp>
#include <covscript/cni.hpp>
// STL输入输出库
#include <iostream>
// CNI根名称空间
CNI_ROOT_NAMESPACE {
    // C++函数声明
    void test() {
        std::cout << "Hello,world!" << std::endl;
    }
    // CNI函数声明
    CNI(test)
}

结合我们之前介绍的知识,能不能看懂这段代码呢?

编译为Covariant Script扩展

由于开发环境的多样,我们这里不再阐述Covariant Script SDK的安装方法

本教程基于基于Debian Linux的发行版,使用Windows的朋友推荐先使用Windows Subsystem Linux进行开发

首先是安装SDK,请在root环境中运行以下代码或使用sudo执行

# wget http://mirrors.covariant.cn/covscript/covscript-amd64.deb
# dpkg -i ./covscript-amd64.deb

然后,切换到工作目录,即你的代码所在目录,执行以下指令

# g++ -std=c++14 --shared -fPIC -O3 源代码.cpp -o 扩展名.cse -lcovscript

执行完毕后,会在当前目录生成一个文件名为”扩展名.cse”的Covariant Script扩展,接下来执行Covariant Script REPL验证

# cs_repl
Covariant Script Programming Language Interpreter REPL
Version: 3.2.0 Psephurus gladius(Stable) Build 8
Copyright (C) 2019 Michael Lee. All rights reserved.
Please visit http://covscript.org/ for more information.
> import test
> test.test()

程序应在终端输出“Hello,world!”,如下图

这便代表我们的扩展编写正确,Covariant Script调用了扩展中的C++函数

模板元编程之类型萃取(Type Traits)中匹配规则的推导方法

现我们希望在编译期判定

  1. 一个类型是否为常量类型
  2. 一个类型是否为引用类型
  3. 一个类型是否为模板类型

那么如何去做呢?

首先是写出泛匹配,泛匹配的结果一定为失败(false)。在本例中的三个需求都是针对单一类型的萃取,所以泛匹配只需要写一个参数即可:

template<typename> struct is_xxxx {
static constexpr bool result = false;
};

在这里,模板参数的参数名被省略,只需把xxxx改成对应的易读名即可,如is_constant_type、is_reference_type、is_template_class。所有的单一类型萃取都可以使用这个泛匹配,只需根据需要更改result的形式即可。比如在这里我们只需要判断是不是某种类型,所以我们使用bool,相应的特化匹配也只需要写一个即可。

接下来就要开始写特化匹配了。特化匹配一般比较难写,我们在这里分成四步:

  1. 写出最终要匹配的类型的一阶推导式
  2. 补齐对应的参数名和相关语法
  3. 写出对应的模板参数列表
  4. 完成特化匹配

一:写出最终要匹配的类型的一阶推导式

推导式的形式一般是:

元素 -> 语法描述

那么推导式的作用就是描述使用“语法描述”分解“元素”的方式,我们将这种方式成为文法。编译器会根据推导式描述的文法对程序进行解析,这种解析的过程称为匹配。

一阶推导式仅涉及到一次匹配,一般比较好写,但以后为了解决更复杂的问题我们可能要写高阶推导式甚至递归推导式,其中递归推导式涉及到消除无限递归的问题,我们以后会讲。

在这里,对应三个例子,有三个一阶推导式:

  1. ConstT -> const T
  2. RefT -> T& | T&&
  3. TemplateClass -> T<…>

注意,“…”指的是任意参数。后两个一阶推导式理论上是递归推导式,但我们不关心接下来的递归结果,所以也可以看作一阶推导。

问题:请列出下面几个类型符合的推导式以及推导结果

  1. const int&
  2. const std::map<std::string, std::size_t>

答案:

  1. 符合ConstT和RefT两个推导式,推导结果分别是T=int&、T=const int
  2. 符合ConstT和TemplateClass两个推导式,推导结果分别是T=std::map<std::string, std::size_t>、T=const std::map

二:补齐对应的参数名和相关语法

一般来说,需要补齐的有以下几个情况:

  1. 任意参数,需要用模板参数包表示出来
  2. 嵌套匹配,需要加上传递给嵌套匹配的参数

在这里需要补齐的只有第三个,补齐后的形式为:

TemplateClass  ->  T<X...>

三:写出对应的模板参数列表

这一步就比较简单,只需按照模板参数的语法规则对照写就可以了:

  1. template<typename T>
  2. template<typename T>
  3. template<template<typename…ArgsT>class T, typename…X>

其中,第三个模板参数列表还可以简化:

template<template<typename...>class T, typename...X>

为什么可以简化?因为这里ArgsT并没有实际意义,仅为了标记模板类参数的模板参数列表。

四:完成特化匹配

到这里我们只需要将我们完成的部分填入下面的模板:

模板参数列表 struct is_xxxx<推导式语法描述> {
static constexpr bool result = true;
};

在这里,既然是特化匹配,匹配结果必然是成功(true);除了填入完成的部分,还要把xxxx改成与泛匹配对应的易读名;像第二个判断类型是否为引用的特化匹配因为存在两个情况所以需要写两个特化匹配。

所以我们最终完成的代码如下:

// 一个类型是否为常量类型
template <typename T>
struct is_constant_type
{
static constexpr bool result = false;
};
template <typename T>
struct is_constant_type<const T>
{
static constexpr bool result = true;
};
// 一个类型是否为引用类型
template <typename T>
struct is_reference_type
{
static constexpr bool result = false;
};
template <typename T>
struct is_reference_type<T &>
{
static constexpr bool result = true;
};
template <typename T>
struct is_reference_type<T &&>
{
static constexpr bool result = true;
};
// 一个类型是否为模板类型
template <typename T>
struct is_template_class
{
static constexpr bool result = false;
};
template <template <typename... ArgsT> class T, typename... X>
struct is_template_class<T<X...>>
{
static constexpr bool result = true;
};

此代码符合C++11标准