0%

有序性

这里说的有序指的是维持键的插入顺序,并不是指使用二叉树维持键的大小关系。

As of Python version 3.7, dictionaries are ordered. In Python 3.6 and earlier, dictionaries are unordered.

OrderedDict 一直是有序的。

https://devdocs.io/python~3.11/library/collections#collections.OrderedDict

比较

  1. Python 内置的字典主要用于查询(但在 3.7 版本之后也附带了维持插入顺序的功能),如果要频繁使用排序的功能,OrderedDict 性能更好。
  2. OrderedDict 有一些排序的接口,方便实现 LRU 缓存等。
  3. OrderedDict 在相等比较时会额外比较键的顺序。

测试方式

用 Windows 电脑向阿里云的 223.5.5.5 服务器发送请求。本文只提供一个直观的比较,并不追求全面的讲解。

DOH

端口号 443。

接口比较复杂,要求我们制作一个 dns 查询包,然后将其转换成 base64 编码,再去掉末尾多余的 =(base64 规定不足 4 字节的整数倍则需要补充 =)。然后将得到的字符串作为 dns 参数放在 url 中。返回的结果也是二进制数据。

curl -s -o output.txt https://223.5.5.5/dns-query?dns=A6QBAAABAAAAAAAAB2FsaWJhYmEDY29tAAABAAE -w "%{time_total}s\n"
  • Windows 自带 curl:50 ms
  • MSYS2 curl:80 ms
  • WSL curl:65 ms

  • exit:退出整个 cmd.exe
  • exit /b:只退出当前的批处理脚本。
  • xx.bat:转去运行 xx.bat,不再返回。
  • call xx.bat:转去运行 xx.bat,结束后返回(除非调用了 exit 使得整个 cmd.exe 中止)。
  • cmd /c yy:执行 yy 命令,结束后在交互界面继续等待操作。
  • cmd /k yy:执行 yy 命令,结束后退出。
  • start zz:创建一个新的窗口运行 zz

如果想要在子脚本有 exit(而不是 exit /b)的情况还能不退出,可以使用 cmd 或者 start 创建新的命令行提示符。

"python" "%~dp0%~nx0"
exit

"""
字符串对于 cmd 来说是外部命令或参数,对于 python 来说是可拼接的字面量。
exit 对 cmd 来说是退出,对于 python 来说是一个函数名。
exit 会中止调用者,但在桌面点击运行的环境下可以正常使用。
"""

import uuid
import subprocess

def main():
    while (input('Hit enter to generate another key: ').strip() == ''):
        s = str(uuid.uuid4())
        subprocess.run("clip", text=True, input=s)
        print(f'{s} is sent to your clipboard!\n')

if __name__ == '__main__':
    try:
        main()
    except:
        pass
    input('\nYou entered something else. Program will end after your next input.')

列表构造顺序

libstdc++ 用的是递归反向构造,尾部的参数先构造。与之对应,libc++ 用的是 parameter pack 继承,是正向构造。

下标编号

两者都使用了下标编号。这样即便 std::tuple 的某两个参数类型相同,也能通过不同的下标把两个元素区分开。

EBCO

两者都有 EBCO 技术。假定 Base<T, bool = InheritableEmptyBase<T>::value> 为两个库所用的元素包装类,Base 在参数为空类且能够被继承时,采用继承的方式存储元素,否则用成员变量存储元素。

但是由于元组参数构造顺序不同,两种库实现占用的空间也可能不同。考虑类型 std::tuple<A, char, A, char, B>,其中 AB 都是空类型,它在 libc++ 中只需要占用 2 个字节。

而 libstdc++ 中同样的类型需要占用 3 个字节:

涉及的 libstdc++ 源码文件:bits/std_function.h

印象: std::function 做了小对象优化,同时在避免使用虚函数(尽管它可以用继承和虚函数来实现)。

关于成员指针,见 Pointer to Member

存储结构

首先看 std::function 的成员组成:

void swap(function& __x) noexcept
{
    std::swap(_M_functor, __x._M_functor);
    std::swap(_M_manager, __x._M_manager);
    std::swap(_M_invoker, __x._M_invoker);
} // 模板太长了,成员定义很分散,抄起来很累;这个 swap 函数写的刚刚好

相关文件:

  • include/set 头文件按顺序引入了 include/bits/stl_tree.hinclude/bits/stl_set.h
  • include/bits/stl_set.h 定义了 std::set,依赖了 include/bits/stl_tree.h 却没有明确包含它。
  • include/bits/stl_tree.h 定义了诸多类型,如未明确指出则默认下面提到的类型都是来自于这个头文件
  • src/c++98/tree.cc 实现了红黑树用到的非模板算法。这个文件只有 400 多行。

std::set 将实现转发给 _Rb_tree 处理,而它又将实现转发给了 _Rb_tree_impl 处理。

_Rb_tree_impl 继承于:

  • _Node_allocator:是一个 typedef,实际类型是一个模板类,模板参数和 _Rb_tree 的参数有关。
  • _Rb_tree_key_compare:是一个模板类,模板参数和 _Rb_tree 有关。
  • _Rb_tree_header非模板类。包含了 _Rb_tree_node_base 结构和 _Rb_tree_node_count。前者是表示红黑树结点的聚合类,包含了 parent/left/right/color 四个重要成员;后者是结点计数。初始化时,parent/left/right 都被设置为 header 本身的地址,而 color 被设置为红色,可见这个 header 就是标准库红黑树中的 nil 哨兵。另外,left 和 right 还有最小结点和最大结点的含义。

下面都是在 Linux 上测试,处理器架构为 x86-64,用的 Itanium C++ ABI

Caution

下面很多指针转整数用的是 long,实际上用 intptr_t 更严谨。

指向成员函数的指针:2 倍大小

在 64 位下是 16 字节,因为需要支持虚函数调用。指向成员函数的指针会保存两部分数据:

ptr:非虚函数则存储函数地址。虚函数则存储虚表中偏置加 1(1 plus the virtual table offset (in bytes) of the function, represented as a ptrdiff_t

adj:存储对 this 指针的调整量。

TL;DR 性能开销

在不开启优化选项时编译:

  1. 每次调用虚函数比普通函数多两次访存。
  2. 每次动态类型转换(除非类型精确命中)都是耗时的图搜索算法。
  3. 明确需要精确转换时用 std::type_indexstatic_cast 会更快。

相关代码下载:

git clone -n --depth=1 --filter=tree:0 https://github.com/gcc-mirror/gcc.git
cd gcc
git sparse-checkout set --no-cone "libstdc++-v3/libsupc++/"
git checkout

虚表内存结构

代码

首先:在共享代码区域存储的虚表记录了特定类的信息。对象中的虚表指针其实是虚函数表指针,指向虚表中的一块位置。