0%

雖然之前有看過,不過看過即忘,還是得寫下來

What’s projection

從一個寫到爛的範例開始

1
2
3
4
5
6
7
8
struct Person {
std::string name;
int age;
};
std::vector<Person> persons;
std::sort(begin(persons), end(persons), [](const auto& p1, const auto& p2) {
return p1.name < p2.name;
});

相信這樣的程式碼已經寫到吐了
如果用C++20 Ranges寫的話可以這樣寫

1
std::ranges::sort(persons, std::ranges::less{}, &Person::name);

可以知道我們要比的就是name,而這樣的寫法就叫做Projection

Backport to C++17

其實要backport到更之前的版本也行, 只要有第三方或是自己寫的invoke
然後寫一個projecting_fn functor,compose以下的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <typename Function, typename Projection>
class projecting_fn {
public:
projecting_fn(Function function, Projection projection)
: m_function{ std::move(function) }
, m_projection{ std::move(projection) }
{
}

template <typename... Args>
decltype(auto) operator() (Args&&... args) const
{
return std::invoke(
m_function,
std::invoke(m_projection, std::forward<decltype(args)>(args))...);
}

private:
Function m_function;
Projection m_projection;
};
std::sort(begin(persons), end(persons),
projecting_fn{ std::less{}, &Person::name });

Projection and view filter

像這樣的Source Code是無法通過編譯的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<typename T>
struct LessThan
{
bool operator()(const T& x){
return x < value;
}
T value;
};

struct Apple
{
int weight;
int size;
};

int main()
{
auto apples = std::vector<Apple>{{1,2}, {2,3}, {3,4}};

auto smallApples = apples | views::filter(LessThan{3}, &Apple::size);
}

解決方式有兩種

不用Projection也是一種解決方法

1
apples | views::filter([] (Apple& a) {return a.size < 3;})

不過這方式就與本文無關了

Boost HOF

1
apples | views::filter(hof::proj(&Apple::size, LessThan{3}));

這方法就類似上面C++17的projecting_fn

Reference

The simplest module

先看範例,就是Module版的Hello World

1
2
3
export module hello;
export void hello_world() {};
void non_export_func() {}

而Consumer Module的一方就這樣寫

1
2
3
4
5
6
7
import hello;
int main()
{
hello_world(); // OK
non_export_func(); // Cannot compile
return 0;
}

Description

從這個範例當中,Consumer這邊不用特別說
這邊要說的是如何寫個Module

Module Unit

在C++20,有了一個新的Compile Unit,就是Module Unit,所有Module Unit的Top Level Statement都是有module關鍵字的
module前面有沒有export就是決定這是哪一種Module Unit

  • export的叫作Module Interface Unit
  • export的叫做Module Implementation Unit

Module Implementation Unit後面再說

The content of a module

一個Module擁有

  • 一個以上的Module Interface Unit
  • 零個以上的Module Implementation Unit

且每個Module裡面有且唯一一個Primary Module Interface Unit

在Hello World這個範例當然只有Primary Module Interface Unit 的存在,至於什麼是Primary Module Interface Unit,也是後面再說

export

在上面的範例,我們定義了兩個函數

1
2
export void hello_world() {};
void non_export_func() {}

不塗於傳統的header file方式,如果是傳統的header file,兩個function應該都可以被外界可見,而Module Unit只有export出的符號才能輩Connsumer看到
export的其他用法還有這樣

1
2
3
4
5
6
7
8
// export entire namespace
export namespace hello {}

// export the symbols in the block
export {
int e = 1;
void test() {}
}

Module Implementation Unit

就像傳統header/implementation的方法,我們可以把declaration/implementation分離,因此我們有了Module Implementation Unit
重寫我們的範例,將implementation分開
因此我們的Module Interface Unit就變成

1
2
3
export module hello;
export void hello_world();
void non_export_func();

而Module Implementation Unit則是

1
2
3
module hello;
void hello_world() {};
void non_export_func() {}

如同之前所說的,module前面沒加export的就是Module Implementation Unit,而在function implementation前面也沒加export,就跟傳統的方式很像

My thought on Module Implementation Unit

之前declaration/implementation被人詬病的一點,就是你要維護兩份狀態,當你declaration改了之後,如果implementation沒改,會產生不可預料的後果,運氣好的話是編譯不過,運氣不好產生深層的Bug更難解

如同之前所說的,一個Module可以不必擁有Module Implementation Unit
那存在的必要是什麼?

我認為是將舊有的Source Code Mitigation到C++ Module的方式
如同現在流行的header only library一樣,未來的Module應該僅由Module Interface Unit組成

Import other module

寫Module時不免使用到其他Module,讓我們定義一個新的Module

1
2
export module world;
export struct obj {};

而我們的hello module就變成這樣

1
2
3
export module hello;
import world;
export void hello_world(obj o) {};

注意,import只能放在top level module declaration之下,不能交換順序

接著要回去看Consumer的部分了

Visibility control

此時我們的Consumer會是這樣

1
2
3
4
5
6
7
8
import hello;
import world;
int main()
{
obj o;
hello_world(o);
return 0;
}

這裡該注意的點,在hello module當中雖然import了world,但是不
會再次輸出symbol到hello module metadata中
因此如果Consumer沒加上import world時,會發現找不到obj的情形

但如果我們將hello改成這樣

1
2
3
export module hello;
export import world;
export void hello_world(obj o) {};

這邊將我們import進來的Module再度export出去,這也是我們細分module的基礎
那麼Consumer不加import world也是可以正常運行

Divide module into small parts

當一個Module大起來之後,要降低複雜度,細分成更小的Block是需要的,而其中又有兩種方法

Sobmodule

我們將hello_world分成兩個function
一個放在hello.sub_a,另外一個放在hello.sub_b
直接看程式碼

1
2
export module hello.sub_a;
export void hello() {};

而另外一個就不貼了,看看我們hello module的定義

1
2
3
export module hello;
export import hello.sub_a;
export import hello.sub_b;

Reexport出hello.sub_ahello.sub_b的exported symbol

Note

hello.sub_ahello_sub_b是各自獨立完整的Module,submodule機制只是邏輯組合,讓他們看起來像是同一個Module
所以你Consumer這樣寫也是可以的

1
2
3
4
5
6
7
8
import hello.sub_a;
import hello.sub_b;
int main()
{
hello();
world();
return 0;
}

Module partition

不同於submodule,partition所分的sub partition不能個別存在
一樣直接看程式碼

1
2
export module hello:part_a;
export void hello() {};

跟上面很像,不過將.改成了:
而我們的hello module則是

1
2
3
export module hello;
export import :part_a;
export import :part_b;

這邊有幾點要注意的

  • 一個module name當中沒有:出現的就是Primary Module Interface Unit,如同之前所說
    一個以上的Module Interface Unit,有且唯一一個Primary Module Interface Unit
    這個範例有三個Module Interface Unit,只有hello是Primary Module Interface Unit
    hello.sub_a則是一個獨立的Module,只是邏輯上看起來是同一個Mdoule

  • Partition只能接受import :part_a的語法,import hello:part_a是不對的

  • Consumer只能寫import hello

Global Module Fragment

Global Module Fragment是提供preprocessor使用的空間,因此你可以在這邊定義Marco,或是include未被moduleized的header file,而在這邊定義的symbol則不會輸出到module interface中,因此不會汙染全局環境

Global Module Fragment必須在export module之前,就像這樣

1
2
3
4
5
module;
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#include <string>
#include <vector>
export module hello;

Reference

前言

基本上這個要求蠻奇怪的,ASIO又不是沒提供Synchronize API,不過有些事情就是只有Asynchronous API能做到
例如我要在五秒鐘之內連線,五秒鐘之內無法連上就直接結束,如果用Synchronize API,Timeout由作業系統決定
這個時候就只有自己寫了

use_future

ASIO有一個feature,可以將Async operation轉成Sync operation
一般來說我們的程式碼會寫成這樣

1
2
3
socket.async_connect(endpoint, [](std::error_code ec) {
// blablabla
});

但是如果我們用use_future的話,ASIO內部會自己轉成promise/future的Pattern
這適合在Threead synchronize的情景使用

1
2
3
4
5
6
7
asio::io_context ctx;
asio::ip::tcp::socket socket(ctx);
auto future = socket.async_connect(endpoint, asio::use_future);
std::thread t([&] {
ctx.run();
});
future.get();

Combie with C++20 Coroutine

如果我們的條件更複雜,如一開始寫的五秒鐘Timeout這件事,上面的程式碼就不敷使用,
如果用原先的Function callback方式寫大概會死一堆腦細胞,而Coroutine可以讓我們大大減輕心智負擔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
asio::awaitable<void> timeout(std::chrono::seconds seconds)
{
asio::steady_timer timer(co_await asio::this_coro::executor);
timer.expires_after(seconds);
co_await timer.async_wait(use_nothrow_awaitable);
}

asio::awaitable<std::error_code> connect_with_timeout(
asio::ip::tcp::socket& socket,
const asio::ip::tcp::endpoint& endpoint)
{
using namespace asio::experimental::awaitable_operators;
auto result = co_await(
socket.async_connect(endpoint, use_nothrow_awaitable) ||
timeout(std::chrono::seconds(5))
);
if (result.index() == 1) {
co_return asio::error::timed_out; // timed out
}
auto [r] = std::get<0>(result);
co_return r;
}

asio::io_context io_context;
auto connect_future = asio::co_spawn(
io_context.get_executor(),
connect_with_timeout(asio::ip::tcp::socket(io_context), endpoint),
asio::use_future);
io_context.run();
return connect_future.get();

如上面程式碼寫的一樣
connect_with_timeout有兩種可能,一個是socket connect的結果,另外一個是timeout
asio::co_spawn的最後一個參數不是教學中的detach,而是剛剛講的use_future
這樣子就可以把Coroutine 和 promise/future一起使用

eBPF和bcc的介紹文件已經有不少了,多寫介紹實在是浪費資源
直接紀錄架構和該怎麼用,先有個概念,日後如果有需要的話再仔細研究

The artitecture of eBPF

一圖勝千文

What is bcc?

  • 由於直接編寫eBPF難度很高,bcc提供了一個Python library,簡化eBPF的開發過程
  • bcc也納入了很多可以直接拿來用的Application

以下是bcc Tracking Tools的示意圖

Write a bcc program

只是個Hello World的範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/python3

from bcc import BPF
from bcc.utils import printb

# define BPF program
prog = """
int hello(void *ctx) {
bpf_trace_printk("Hello, World!\\n");
return 0;
}
"""

# load BPF program
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")

# header
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "MESSAGE"))

# format output
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
continue
except KeyboardInterrupt:
exit()
printb(b"%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))

Reference

書籍

之前遇到亂流,需要重新找工作,如今告一段落,可以寫點東西了

來聊聊Empty Struct的問題好了

Empty struct

1
2
3
struct empty {
};
printf("%ld\n", sizeof(struct empty)); // ????

這個答案有所不同
在C語言,印出來的情形是0
在C++,印出來會是1,C++為了保證不同的Object的Address不同,就算是empty struct, sizeof也不為空

Embedded empty struct

那如果是這樣呢

1
2
3
4
5
6
7
8
struct empty {
};

struct non_empty {
int v;
struct empty e;
};
printf("%ld\n", sizeof(struct non_empty)); // ????

同樣的
在C語言,印出來的情形是4
在C++,印出來當然不會是4,在我的ubuntu 64bit印出來是8

Why use empty struct

在C語言的應用情景,empty struct沒有任何用途
可是在C++的世界裡面,empty struct可以是個functor
例如std::lessstd::equal_to之類的
使用template class可以將functor傳入struct裡面,因此可以擴充這個class的功能

How to reuduce the size

既然有empty struct的使用場景,又不想浪費多餘的空間,所以就有人想出這樣的方法

1
2
3
4
struct non_empty : empty {
int v;
};
printf("%ld\n", sizeof(struct non_empty)); // ????

這下就如我們預料的是4了

The problem of Inherence

Leak Interface

由於Inherence有很強的傳染力,Parent class的Public API都能背Child class自由使用,因此可以寫出這樣的程式碼

1
2
3
4
5
6
7
8
9
class empty {
public:
int f() { return 42; }
};
class non_empty : public X {
int v;
};
non_empty obj;
obj.f();

可是我不想讓obj直接f函數…該怎麼做

1
2
3
4
5
6
7
8
9
10
11
12
13
class empty {
public:
int f() { return 42; }
};
class X : empty {
public:
empty& get() { return *this; };
};
class non_empty : public X {
int v;
};
non_empty obj;
obj.get().f();

要使用f只能透過get來做了
這也是boost empty_value在做的事情

Hard to reason sometimes

這也是我看了程式碼才能體會到的事情
以下是從boost intrusive中節錄的片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<class ValueTraits, class VoidOrKeyOfValue, class VoidOrKeyHash, class VoidOrKeyEqual, class BucketTraits, class SizeType, std::size_t BoolFlags>
struct hashdata_internal
: public hashtable_size_traits_wrapper
< bucket_hash_equal_t
< ValueTraits, VoidOrKeyOfValue, VoidOrKeyHash, VoidOrKeyEqual
, BucketTraits
, 0 != (BoolFlags & hash_bool_flags::cache_begin_pos)
> //2
, SizeType
, (BoolFlags & hash_bool_flags::incremental_pos) != 0
>
{
typedef hashtable_size_traits_wrapper
< bucket_hash_equal_t
< ValueTraits, VoidOrKeyOfValue, VoidOrKeyHash, VoidOrKeyEqual
, BucketTraits
, 0 != (BoolFlags & hash_bool_flags::cache_begin_pos)
> //2
, SizeType
, (BoolFlags & hash_bool_flags::incremental_pos) != 0
> internal_type;;
};

hashtable_size_traits_wrapper依設定不同,可能是個empty struct
上面這段,重歷的程式碼出現了兩次,又臭又長,難以理解

[[no_unique_address]]

C++20引進了一個很有用的attribute,這告訴Compilier,不必為這個object特別分配一個Address,因此有了無限可能

No need inherence

雲本需要用Inherence辦到的事情

1
2
3
4
5
6
7
8
9
10
11
12
13
class empty {
public:
int f() { return 42; }
};

class non_empty {
int v;
[[no_unique_address]] empty e;
public:
empty& get() { return e; }
};
non_empty obj;
obj.get().f();

Fix hard to reason issue

以上面那段Code來舉例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class ValueTraits, class VoidOrKeyOfValue, class VoidOrKeyHash, class VoidOrKeyEqual, class BucketTraits, class SizeType, std::size_t BoolFlags>
struct hashdata_internal
{
typedef hashtable_size_traits_wrapper
< bucket_hash_equal_t
< ValueTraits, VoidOrKeyOfValue, VoidOrKeyHash, VoidOrKeyEqual
, BucketTraits
, 0 != (BoolFlags & hash_bool_flags::cache_begin_pos)
> //2
, SizeType
, (BoolFlags & hash_bool_flags::incremental_pos) != 0
> internal_type;;
[[no_unique_address]] internal_type size_traits_;
};

雖然還是又臭又長,不過已經改善不少

Trap on [[no_unique_address]]

以下的程式碼會是如何

1
2
3
4
5
6
7
8
class empty {
};
class non_empty {
int v;
[[no_unique_address]] empty e;
[[no_unique_address]] empty e1;
};
printf("%ld\n", sizeof(struct non_empty));

答案當然不是4,e和e1是同一個type,為了區分,不所以只能有一個有[[no_unique_address]]的屬性
要修掉這問題也很簡單

1
2
3
4
5
6
7
8
9
template <int>
class empty {
};

class non_empty {
int v;
[[no_unique_address]] empty<0> e;
[[no_unique_address]] empty<1> e1;
};

Another issue on [[no_unique_address]]

目前[[no_unique_address]]在MSVC是沒效果的,會造成ABI Break

Asynchronous programming

Why asynchronous programming

Asynchronous programming 是個反人類的思考的東西,就算選擇不同的程式語言,共識最好的Network programming model,都是這個樣子,一個connection一個thread

1
2
3
4
5
6
7
8
9
10
11
listen(socket_fd, 20);

/* Looooop */
while (1) {
newsocket_fd = accept(socket_fd,
(struct sockaddr *) &client_addr,
&client_len);
pthread_t thread;
pthread_create(&thread, NULL, run_thread, (void *) newsocket_fd);
pthread_join(thread, NULL);
}

這個Model可以解決95%的問題,不過人生最難的就是那個But,這個Programming Model不能Scale

C10K Problem (1999)

這就是著名的C10K Problem,是Operation System的問題,OS不能有跟Connection一樣多的Thread,就算可以,也會耗費大量的Memory,以及頻繁的Context Switch
山不轉路轉,於是出現了IO multiplexing技術,也就是大家熟知的select/poll/epoll

The early stage of asynchronous programming

一開始的asynchronous programming,就算是libuv,asio或是nodejs等,都需要一個callback當參數,寫著寫著就會變成這樣

The problem of callback

  • 反人類

Thread based solution之所以被推崇,就是人類的思考模式傾向於直線思考,而Callback based solution需要將步驟切得七零八落,慘不忍睹

  • 難寫易錯

假設事務夠簡單,一兩層callback就能解決的話,事情還好辦,當邏輯複雜到一個程度,寫錯的機率實在是太高了

Source Code是要寫給人看的,因此需要有工具來管理複雜度,也就是Coroutine

System Language對於Coroutine的態度

  • C:不關我的事,你自己想辦法
  • C++: 到了2021年還沒有標準的Network Library:會不會太落後
  • Rust: 比C++早訂定標準:不過押寶押錯了:標準也定了:改不了了:至於押寶押錯這件事後面再說

What is coroutine

太陽底下沒有新鮮事,Coroutine在1963年就被提出,過了五十年後重新被人想起
Coroutine擁有以下四種特性

  • Invoke
  • Return
  • Yield
  • Resume

而我們一般所知道的Function就只有

  • Invoke
  • Return

也就是Function只是Coroutine的特殊案例

Coroutine的另一項特性

  • Cooperative multitasking

同樣的,太陽底下沒有新鮮事
聽過當初Windows 3.1常常會程式卡死,而Windows 95不會,就是因為將Cooperative multitasking改成Pre-emptive multitasking

The simplest example on coroutine

雖然這範例沒什麼用,不過能夠讓我們了解Corotuine的本質,能夠Yield和Resume
switch的case可以包含在for loop迴圈裡面,不過蔗不是本文重點

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int counter(void) {
static int i, state = 0;
switch (state) {
case 0: /* start of function */
for (i = 0; i < 10; i++) {
state = 1; /* so we will come back to "case 1" */
return i;
case 1:; /* resume control straight after the return */
}
}
}
int main()
{
for (int i = 0; i < 10; i++)
printf("%d\n", counter());
}

上面這個只是個玩具Coroutine,真正能拿來用的還分幾類
至於怎麼做就各顯神通了

Two difference model on Coroutine

就算是Coroutine,也可以分成兩類

  • Stackful Coroutine
  • Stackless Coroutine
    顧名思義,差異就在對Stack的處理上面
  • Stackless將State放在Heap上,而Stackful放在Stack上
  • Stackless的大小是動態分配的,Stackful的Stack是固定大小的
  • Stackless本質是個StateMachine,而Stackful是個User Mode Thread
    因此Stackess Machine的Runtime消耗比較小,Stackful相反
  • Stackful可以和舊有的synchronous code組合,Stackless不行
  • Stackless需要Compilier支援,Stackful只需要Library就能做了
  • Stackless的方案有傳染性,例如你在Javascrupt所看到的
    1
    2
    3
    async func1() {
    await func2();
    }
    你的async/await是成雙成對的,布這麼用就會出錯,而Stackful沒有此限制
  • Stackful的程式好寫,Stackless需要一定能力

選邊站

由於兩種Model差異很大,由於程式語言的特性以及歷史因素,不同程式語言的選擇也不一樣

  • Stackless:C#(第一個使用async/await的主流語言),Javascript,Python,C++,Rust,Kotlin(雖然是JVM的語言,不過跟Java選擇不同)
  • Stackful:Golang(其實是變種的Coroutine),Java(照抄Golang那套,不過還沒推出),PHP(in the future)

Goroutine

前面提到,Goroutine是Stackful Coroutine的變形,最主要的差異在於

  • coroutine是順序執行
  • Goroutine可以在多個cpu平行執行的
    因此又產生了分歧點
    假設我們有Coroutine A,B,C
    C等待B的資料,B等待A的資料
  • 如果是傳統的Coroutine,A執行完會transfer到B,B執行完會transfer到C,由於在同一個CPU上,資料不用加鎖
  • 如果是Goroutine,A,B,C三者可能在不同的CPU上跑,關於資料的傳遞只能透過Channel
  • 由於Golang實作了一個有效利用Cpu Usage的Runtime,將corotuine定義成light weight thread,所以Golang Runtime需要做一部分OS需要做的事情,例如Schedule coroutine
  • Mandatory goroutine,就算你寫一個hello world也避不掉
  • Goroutine不快,Maximum network connection也比不上Stackless Coroutine(C++/Rust)
  • 不過程式好寫太多,這強項才是goroutine搶走PHP/Python的主要原因

押錯寶

講講Rust押寶押錯的故事

IO Model有兩種

如同Coroutine有兩種,IO EventLoop也有兩種

  • Proactor:最著名的就是Windows的IOCP了
  • Reactor:select/poll/epoll等都是
    Rust使用epoll的Reactor Model,不過epoll不是linux的未來

CPU Spectre and Meltdown

就跟COVID-19一樣,Spectre和Meltdown改變了寫程式的方向
因為CPU的Bug,Linux修正方向,io_uring才是Linux的未來,而io_uring和IOCP一樣,是Proactor的model

Influence

由於標準定了,要改改不了了
如果要改的話只有兩種選擇

  • 重新制定標準,然後變成v2版本,光是制定一個版本花了四年,這次應該會快一點
  • 兩個Model是可以互轉的,只是會有Performance Loss,當Spectre和Meltdown的Patch打上去之後會掉多少更難以估計

Conclusion

  • 如果你是那95%的人,根本用不上Asynchronous programming,直接使用thread model,還不容易錯
  • 如果不幸是那5%的人,首先考慮golang,golang就算幾千個缺點,goroutine都能掩蓋過去
    golang適合寫網路服務,也只能寫網路服務
  • 如果你是一秒鐘幾千萬上下,出來跑得遲早都要還,逃不掉C/C++/Rust寫code了
    這裡有個實際案例
    Why Discord is switching from Go to Rust
  • 沒有最好的方案,只有適合的方案

Coroutine的文章太難寫了,只好先寫篇簡單的
這是一篇工程性的文章,給對這方面有興趣的人

Motivation

由於看了某AI部門寫的C++服务编译耗时优化原理及实践這篇文章,想要分享一下思路 ,不過編譯二十分鐘就在哀哀叫 (當初我前公司動輒一兩個小時

Clarification

  • 這問題跟C++無關,這是C語言的問題
  • C++的編譯模型跟C語言一樣(Before C++20)
  • 不過由於C++有template和header only libraries而將這問題放大了很多倍
  • 不信的話可以試試看編譯Linux kernel

Root Cause

哪有什麼Root Cause,這不是個Bug

只是跟不上時代

C語言誕生在1969-1973,至今超過五十年了
當初計算機能力比不上現代,因此產生了
Header / Implementation Separation的做法
C++繼承了這個Compiling Model

Modern Language怎麼做

將宣告和實做擺在一起

  • Application Language: Java/C#/Javascript/Golang
  • System Language: Zig/Rust

About Future

分兩方面來說

C

C語言已經是個Inactive的語言了

  • 自從C11之後,不加入重大新功能,只做相容性改善
  • 別期待它會加入Module功能

C++

相較於C,C++從11之後努力追趕Modern Language的路線

  • 直到C++20之後,才推出正式的Module Spec
    • 不過何時可以用上未可知
    • Cloud端也許明年就能使用,embedded等vendor改朝換代不知道要多久
    • 就算全面普及之後,ecosystem也要一段時間才能趕上
    • 以Javascript為例,從commonJS切到Module也走了五六年

What can we do now

未來會怎麼發展不知道,不過現在有幾個選項

什麼都不要做

這不是Bug,不管它也無妨,如果真在意編譯速度的話,當初我前公司買Incredibuild做分散式編譯
根據最前面那篇文章的數據,使用分散式編譯效果比其他方法都有用

Precompiled Headers

將常用的header通通include在一起,然後編譯這個大的header file
有興趣可以參考Using pre-compiled headers in GCC/Clang using CMake and usage in Catch2

Reduce Header dependency

這可以分成幾方面來討論

Find unused header

舉個例子

1
2
#include <stdio.h>
int add(int a, int b) { return a + b; }

stdio.h在這邊就是完全沒必要的,如果是goalng,import fmt然後沒用到根本編譯不過,不過golang這作法是個雙面刃,我不喜歡

如果要工具的話可以考慮[include-what-you-use](

Choose suitable third party libraries

不可能所有東西都自己寫,當你需要某個功能的時候,先找找是否有適合的選項,問題是如果選擇太多了該怎麼辦,總不能

文中討論到Boost,Boost最大的問題是它依賴性太重了
當你需要一台腳踏車的廠警,選擇一台坦克車絕對不是個好主意

Opaque Pointer

俗稱編譯防火牆的技術,是當的斷開header dependency
以C語言來說,header大概長這樣

1
2
3
4
5
struct obj;

size_t obj_size(void);
void obj_setid(struct obj *, int);
int obj_getid(struct obj *);

implementation是這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "obj.h"

struct obj {
int id;
};

size_t obj_size(void) {
return sizeof(struct obj);
}

void obj_setid(struct obj *o, int i) {
o->id = i;
}

int obj_getid(struct obj *o) {return o->id; }

C++版的叫pImpl,如果有看過我的程式碼應該不陌生,掠過

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <memory>

class PublicClass {
public:
PublicClass(); // Constructor
PublicClass(const PublicClass&); // Copy constructor
PublicClass(PublicClass&&); // Move constructor
PublicClass& operator=(const PublicClass&); // Copy assignment operator
PublicClass& operator=(PublicClass&&); // Move assignment operator
~PublicClass(); // Destructor

// Other operations...

private:
struct CheshireCat; // Not defined here
std::unique_ptr<CheshireCat> d_ptr_; // Opaque pointer
};

Conclusion

雖然這不是個Bug,不過有人題就表示這是個需求,未來會怎麼走沒人知道,只能現有的材料下能做些什麼改進

看到很多文章,總覺得從co_await開始講解實在很難清楚表達
寫了自己的版本當作筆記

The simplest coroutine

這程式碼什麼都沒幹,不過就是一個最小的coroutine了
一個coroutine至少要有co_returnco_awaitco_yield其中之一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using namespace std;
#if defined(__clang__)
#include <experimental/coroutine>
using namespace std::experimental;
#else
#include <coroutine>
#endif

struct Task {
struct promise_type {
auto initial_suspend() { return suspend_never{}; }
auto final_suspend() noexcept { return suspend_never{}; }
auto get_return_object() { return Task(coroutine_handle<promise_type>::from_promise(*this)); }
void return_void() {}
void unhandled_exception() {}
};
Task(coroutine_handle<promise_type> h) : handle(h) {}
~Task() {
if (handle)
handle.destroy();
}
coroutine_handle<promise_type> handle;
};

Task coroutineDemo()
{
co_return;
}

int main() {
auto task = coroutineDemo();
return 0;
}

Under the hood

Compilier做了很多事情
像這樣的Psuedo Code

1
2
3
4
5
template <typename TRet, typename … TArgs>
TRet func(TArgs args…)
{
body;
}

被Compilier處理之後大概變成這個樣子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <typename TRet, typename ... TArgs>
TRet func(TArgs args...)
{
using promise_t = typename coroutine_traits<TRet, TArgs...>::promise_type;

promise_t promise;
auto __return__ = promise.get_return_object();

co_await promise.initial_suspend();

try
{ // co_return expr; => promise.return_value(expr); goto final_suspend;
body; // co_return; => promise.return_void(); goto final_suspend;
} // co_yield expr; => co_await promise.yield_value(expr);
catch (...)
{
promise.unhandled_exception();
}

final_suspend:
co_await promise.final_suspend();
}

先忽略co_await的語句,之後補上
因此我們可以看到prmise_type裡面有initial_suspendfinal_suspend等function,這個promise_type是定義coroutine的行為模式
之後會對promise_type做更進一步的說明,這邊就此打住

How the compiler chooses the promise type

看到上面的Pseudo code

1
using promise_t = typename coroutine_traits<TRet, TArgs...>::promise_type;

然後看看coroutine_traits的定義

1
2
3
4
5
6
7
8
9
10
template <class _Ret, class = void>
struct _Coroutine_traits {};

template <class _Ret>
struct _Coroutine_traits<_Ret, void_t<typename _Ret::promise_type>> {
using promise_type = typename _Ret::promise_type;
};

template <class _Ret, class...>
struct coroutine_traits : _Coroutine_traits<_Ret> {};

就是看TRet裡面有沒有promise_type的struct definition了
者李又分成兩類

直接定義在class裡面

就是我們範例那個做法,簡單直接

將promise_type抽出

當你有一群Coroutine,然後這群Coroutine雖然有些許不同,但是對Coroutine的控制流程相同,就可以用這方案

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
struct Promise {
auto initial_suspend() { return suspend_never{}; }
auto final_suspend() noexcept { return suspend_never{}; }
auto get_return_object() { return T(coroutine_handle<Promise>::from_promise(*this)); }
void return_void() {}
void unhandled_exception() {}
};

struct Task {
// Ignore
using promise_type = Promise<Task>;
};

著名的C++ coroutine都使用此方式

coroutine_handle

看到我們的Task當中有coroutine_handle了嗎

1
2
3
struct Task {
coroutine_handle<promise_type> handle;
};

這個才是coroutine的本體,負責讓暫停的coroutine繼續執行,或是判斷coroutine是否執行完畢
將原先的範例加強一下,來示範coroutine_handle該怎麼使用

A failure example

原本我想寫個像這樣的程式加深對coroutine_handle的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
using namespace std;
#if defined(__clang__)
#include <experimental/coroutine>
using namespace std::experimental;
#else
#include <coroutine>
#endif

struct Task {
struct promise_type {
auto initial_suspend() { return suspend_always{}; }
auto final_suspend() noexcept { return suspend_never{}; }
auto get_return_object() { return Task{ coroutine_handle<promise_type>::from_promise(*this) }; }
void return_void() {}
void unhandled_exception() {}
};
coroutine_handle<promise_type> handle;
~Task() {
if (handle)
handle.destroy();
}
void resume() { handle.resume(); }
bool done() const { return handle.done(); }
};

Task coroutineDemo(int times)
{
for (size_t i = 0; i < times; i++) {
cout << "coroutineDemo\n";
co_await suspend_always{};
}
co_return;
}

int main() {
auto task = coroutineDemo(3);
while (!task.done()) {
task.resume();
}
std::cout << "Done\n";
return 0;
}

結果不如預期,發現問題出在

Once execution propagates outside of the coroutine body then the coroutine frame is destroyed. Destroying the coroutine frame involves a number of steps:

  1. Call the destructor of the promise object.
  2. Call the destructors of the function parameter copies.
  3. Call operator delete to free the memory used by the coroutine frame (optional)
  4. Transfer execution back to the caller/resumer.

問題就出在3這步

  • Visual C++不會在coroutine body結束時立刻delete coroutine frame
  • GCC和Clang會,當coroutine frame destroy之後,呼叫handle.done()handle.resume()是use-after-free

Solution

研究了一下, 只要將final_suspend改成

1
2
3
4
struct promise_type {
// ignored
auto final_suspend() noexcept { return suspend_always{}; }
};

讓它在coroutine結束之前停下來就可以了

Reference

Function template overload

拿Boost.Serialization當範例

1
2
3
4
5
6
7
namespace boost::serialization {
template <class Archive>
void serialize(Archive& ar, two_things& tt, unsigned int version) {
ar & tt.a;
ar & tt.b;
}
}

Pros:

  • 就像膠水,將黏貼部分獨立成單一Unit,需要的時候再引入即可

Cons:

  • 跟前個方式依樣,需要打開library的namespace
  • 跟前一個方式依樣,存在Name Collisions的可能

Friend member function

又是Boost.Serialization

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <boost/serialization/serialization.hpp>
struct two_things {
private:
friend class boost::serialization::access;
template <class Archive>
void serialize(Archive& ar, unsigned int version) {
ar & a;
ar & b;
}
public:
int a;
bool b;
};

優缺點正好跟上面相反

Pros:

  • 不需要打開library的namespace
  • 不會造成Name Collisions

Cons:

  • Tightly coupled with structure

當我們如果不需要serialization功能時,透過上面的方式,我們只需要不include implemtation unit即可,不過這方法就不行了
另外一點,這方式引入了boost::serialization的依賴,不管我們需不需要,都必須承受這副作用

ARGUMENT-DEPENDENT LOOKUP

C++最難理解的特性之一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace {
struct two_things {
int a;
bool b;
};
void do_something(two_things &t) {
printf("do_something in anonymous namespace\n");
}
};

int main()
{
two_things t;
do_something(t);
}
Worng usage example of ADL (std::swap)

What’s wrong with swap
看看以下的程式馬

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace A {
struct two_things {
int a;
bool b;
};
void swap(two_things &, two_things &)
{
printf("swap in namespace A\n");
}
};
namespace B {
template <typename T>
void test(T& a, T&b)
{
swap(a, b);
}
}
int main()
{
A::two_things a, b;
int c, d;
B::test(a, b);
B::test(c, d);
}
  • swap(a, b) // invokes ADL because call name is unqualified
  • std::swap(a, b) // does not invoke ADL because call name is qualified

如果我們把swap改成std::stap,自訂億的swap就毫無作用了
正確的做法應該是

1
2
3
4
5
6
7
8
namespace B {
template <typename T>
void test(T& a, T&b)
{
using std::swap;
swap(a, b);
}
}
Correct usage example of ADL (std::swap)
  • Create a callable function object which does the two-step with an internal detail namespace’s swap.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    namespace nonstd {
    namespace detail {
    template <typename T>
    void swap(T&, T&) { printf("nonstd::swap\n"); }
    struct swap_func {
    template <typename T>
    void operator()(T &a, T&b) const noexcept {
    swap(a, b);
    }
    };
    }
    inline constexpr const auto swap = detail::swap_func{};
    }

    namespace B {
    template <typename T>
    void test(T& a, T&b)
    {
    nonstd::swap(a, b);
    }
    }
    這做法在Reference中的文章有特別說明,被稱作Customization Point Object
    亦或是niebloid(以作者的名稱命名的單字)
Friend or not?

假設我們把 namespace A中的swap從free_function變成friend function

1
2
3
4
5
6
7
8
struct two_things {
int a;
bool b;
friend void swap(two_things &, two_things &)
{
printf("swap in namespace A\n");
}
};

兩者是等價的,不過

  • friend function 可以存取 structure的private field,free function不能
  • friend function只能被ADL找到
Downgrade to C++11

inline variable是C++17才有的玩意 在有些時候用不了新標準的情況之下 有必要找個方法向下相容

1
2
3
4
5
6
7
8
namespace nonstd {
// older standards:
template <typename T>
struct __static_const { static constexpr T value{}; };
template <typename T>
constexpr T __static_const<T>::value;
constexpr const auto& swap = __static_const<detail::swap_func>::value;
}

Reference