0%

原文:C-store: a column-oriented DBMS

TL;DR

本文idea:面向ad-hoc读优化的分布式DBMS。

论证过程:

  1. 面向ad-hoc读优化需要:
    1. 列存,以提高压缩和扫描效率;
    2. 数据直接存成多种排序形式,从而应对ad-hoc查询;
    3. 只读事务避免加锁,需要读写分离以及snapshot isolation。
  2. 列存需要仔细地编码数据,因此需要区分不同情况编码。
  3. 数据存成projection需要考虑:
    1. 不同replica可以用不同的projection,这样既满足了安全性,又有机会提高性能;
    2. anchor table物理上不存在,就需要通过storage key和join index组合多个projection来还原一行数据;
  4. 读写分离要考虑:
    1. 分成WS和RS,这样两者可以用不同的存储结构,分别在写和读上达到最优;
    2. 任意时间的MVCC开销太大,可以把时间分成epoch,一段一段推进;
    3. 需要有一个将WS合并进RS的操作。
  5. 考虑到C-Store的主要场景是写特别少读特别多,可以:
    1. 不压缩WS中的数据;
    2. 使用简化的2PC而不需要考虑吞吐问题。

感想:

  1. 学术界的paper画风和工业界的确实不太一样,到处都是idea,但细推敲又觉得缺点什么。
  2. 列存的核心问题就是编码。
  3. 不同replica用不同projection确实是非常棒的idea,但这样要求DB和FS必须耦合在一起,很多情况下不现实。
  4. 关于读写分离的几个idea很棒,MVCC是可以不用那么细粒度的。
阅读全文 »

原文:Michael Stonebraker and Uğur Çetintemel: “‘One Size Fits All’: An Idea Whose Time Has Come and Gone,” at April 2005.

TL;DR

本文idea:DBMS的未来趋势是多个相互独立的DB engine针对不同的细分领域。

论证过程:

  1. 过去若干年中DBMS厂家追求“One size fits all”,原因有若干。
  2. 传统DBMS主要针对OLTP,处理模型为outbound,针对性优化短事务,重点在于ACID,这些特点并不适用于OLAP。
  3. 随着streaming processing兴起,它需要完全不同的优化方向。
  4. 未来使用DBMS的应用类型会越来越多,会有更多针对特定领域的DB engine出现。

感想:

  1. 任何优化都是有代价的,选择针对一个方向优化,就会在另一个方向平庸。甚至提供不同优化的开关也是有代价的,只要允许选择,代价就会存在。
  2. 不同应用需要不同程度的一致性,但一套系统在架构设计时就要锚定某种一致性,它运行在其它一致性级别时只是稍有优化,而不会达到很理想的水平。
  3. 如何控制DB与应用逻辑的耦合度,过高的耦合度会带来安全性、维护性上的问题,而过低的耦合度又会带来性能开销。存储过程,以及各种各样的UDF就是其中的粘合剂。
阅读全文 »

随便看看

哥德尔定理是否支持不可知论? - 罗心澄的回答 - 知乎

数学意义上的「真」和「可证」是两个不重叠的概念。

是否存在「可证明」的对于世界的解释? - oldgoat的回答 - 知乎

一般来说,仅根据经验事实就可以确认某个判断为真,这就叫做“证实”(verify),具有这种特征的判断就叫做可证实(verifiability)的。

如何简单清晰地解释哥德尔不完备定理? - 叶青杰的回答 - 知乎

哥德尔数与不动点。

如何简单清晰地解释哥德尔不完备定理? - LLLBK的回答 - 知乎

他认为自己的不完全性定理表明,存在一些确实为真的东西是人为构造的逻辑系统无法达到的,这些确实为真的东西就是数学的理念世界,它们跟人无关,它们有自己确定的真值,不论人有没有去研究它。

Paradox at the heart of mathematics makes physics problem unanswerable

凝聚态计算spectral gap,当模型为无限大小的2D平面时,无法预测计算是否能结束,即spectral gap是否存在。

如何看待南京一中高考成绩令部分家长不满,家长要求「校长下课」? - 不想上吊王承恩的回答 - 知乎

任何东西都是拥有了才有资格不在乎。像我这种工贼太多了,所以全世界的鸡蛋联合不起来,石头也不允许鸡蛋联合起来。

如何看待杭州某物业私自将业主别墅出租给《我和我的儿女们》剧组拍摄,业主看剧才发现是自己家这件事? - stan的回答 - 知乎

法律常识学习:侵权还是违约?无权代理还是无权处分?合同效力?剧组是否应当承担责任?

为什么金属乐迷大多热衷于将金属从摇滚中独立出来? - 远古邪恶小人物的回答 - 知乎

摇滚乐是一场人类文化运动;类型文化总是具有原教旨倾向。

如何看待美国防长 7 月 21 日谈中美关系时,称「我们不是要寻找冲突」? - 王子君的回答 - 知乎

华盛顿的态度是:我们又不是到摊牌的地步,你们为什么反应这么激烈嘛;北京的态度是:你们不能老是把你们国情的代价转移给我们。

就目前(2017年)的国际局势来看,美国的联俄制中政策会取得成功吗? - 王子君的回答 - 知乎

之所以折腾到今天,美俄还是冷眼互看,主要是内外都有阻力:外部反对;内部阻力。

如何评价睡前消息第147期? - 离离图总裂夫的回答 - 知乎

一个没有核心价值的威权主义组织,有两个问题:第一,领袖稍微不行就一堆破事,组织开始涣散。从商周到罗马以色列,再到俄罗斯台湾全都是这样;第二,没有产生合格领袖的机制。

纲不举的结果,就是目不张。

新加坡其实适合走西方议会制路线的。因为英、美的制度设计,本质就是一种精英寡头政治。西方议会政治建立在社会中产以上有着强共识,而且统治集团人数很少,和中产没有根本利益冲突的前提下。这种制度下,一旦社会出现分裂,立马就混不下去。

要说西方真对我们有什么贡献的话,就是让中国人明白,有时候还是得放下过去。(避免路线依赖)

如何看待「香港或被踢出 SWIFT 银行结算系统」的消息?如果属实,将会造成什么样的影响? - Mason Wu的回答 - 知乎

什么是SWIFT?为什么会有这个谣言?

计算机相关

O’Reilly eBook: Apache Pulsar Versus Apache Kafka - MemSQL

只有33页的新书,各方面对比了一下Pulsar和Kafka,不是特别有新意。

为什么有线网络可以全双工,而无线网络只能半双工呢? - 甜草莓的回答 - 知乎

同时同频(In-band)的无线全双工存在自干扰问题;解决方案收益不大且成本高(增加天线或增加隔离度)。

deno fmt 从 prettier 切换到 dprint,性能提升 10+ 倍 - justjavac的文章 - 知乎

Rust降维打击;wasm性能损耗不低。

读书

技能

  • Dart
  • Java
    • Guice
  • SQL

见识

  • 入关学
  • 新公司的协作方式

思考

  • 如何预测与测量项目的收益
  • 团队内的沟通
  • 两个公司的管理模式的差异
  • 我的职业发展的目标是什么

在将上层容易写的代码转换为高效的由计算机去执行的机器码的过程中,编译器必不可少。但它们在其中完成的复杂工作却常常被人忽视。你也许会花许多时间来慎重考虑算法和解决错误,但可能没有足够的时间关注编译器能做什么。

本文介绍了一些编译器和代码生产方面的概念,之后着重介绍一些你的编译器为你所做的令人印象深刻的转换工作,以及我最喜欢的优化方式的一些实际例子。希望你能了解编译器可以做哪些优化,以及如何进一步探索该主题。最重要的是,你可能也会爱上看汇编输出,并开始对编译器的工程质量肃然起敬。

本文举的都是C/C++的例子,这是我最有经验的语言。但其中的许多优化方法也适用于其它编译语言。事实上,像LLVM这样的前端不可见的编译器工具包的出现意味着多数优化方法都会以相同方式作用在Rust/Swift/D语言等语言上。

阅读全文 »

原文地址

在评估离线应用的性能时,我们通常关心吞吐和平均延时。但在交互/在线应用中,延时长尾对用户体验同样有着非常大的影响。这些应用涉及的机器数越多,数据规模越大,延时长尾越严重,tail-tolerant越重要,就像fault-tolerant一样。

本文介绍了大型系统中常见的几种产生延时长尾的因素,以及如何消除它们。这些方法通常可以借助已有技术,不会增加多少开销。

我在Tablestore做性能优化时,非常明显地感觉到了延时长尾对整体性能/用户体验的影响。这篇文章主要讲的是如何解决准交互(亚秒级)分析的延时长尾,但并没有太多涉及我最关心的Batch写入长尾问题。无论如何,这篇文章都很有意义。

阅读全文 »

原文:Three Optimization Tips for C++

建议:
* Prefer static linking and position-dependent code (as opposed to PIC, position-independent code).
* Prefer 64-bit code and 32-bit data.
* Prefer array indexing to pointers (this one seems to reverse every ten years).
* Prefer regular memory access patterns.
* Minimize control flow.
* Avoid data dependencies.

阅读全文 »

注:本文所使用编译器均为gcc4.1.2,即只能使用C++98标准。

背景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Node {
int mRow;
int mCol;
int mValue;

Node(int row, int col, int value)
: mRow(row), mCol(col), mValue(value)
{}
};

class Iterator {
public:
virtual ~Iterator() {}

virtual Node GetValue() const = 0;
virtual bool IsValid() const = 0;
virtual void MoveNext() = 0;
};

我们有一个接口类Iterator,它有三个方法,可以返回Node类型的值。现在我们要用这个接口来遍历一个矩阵。

矩阵的类型是vector<vector<int>>,我们有几种不同的遍历需求:

1
Iterator* NewRawIterator(const std::vector<std::vector<int> >& matrix);

这个方法返回一个最原始的Iterator,只是单纯遍历数据。

1
Iterator* NewOddIterator(const std::vector<std::vector<int> >& matrix);

RawIterator基础上,只返回奇数。

1
Iterator* NewAppendPerRowIterator(const std::vector<std::vector<int> >& matrix, const std::vector<int>& appendix);

OddIterator基础上,遍历过程中,在每行末尾加上appendix中的元素。

1
Iterator* NewDoubleIterator(const std::vector<std::vector<int> >& matrix, const std::vector<int>& appendix);

AppendPerRowIterator基础上,返回的每个Node的value都乘2。

测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(int argc, char** argv) {
srand(time(NULL));
vector<vector<int> > matrix;
vector<int> appendix;
PrepareMatrix(matrix, 1000, 10000);
PrepareAppendix(appendix, 3);

int64_t t0 = Measure(NewRawIterator(matrix));
int64_t t1 = Measure(NewOddIterator(matrix));
int64_t t2 = Measure(NewAppendPerRowIterator(matrix, appendix));
int64_t t3 = Measure(NewDoubleIterator(matrix, appendix));

cout << "Raw:" << t0 << endl
<< "Odd:" << t1 << endl
<< "Append:" << t2 << endl
<< "Double:" << t3 << endl;
}

我们会在一个1000*10000的矩阵上测试各个Iterator

基于继承+组合的实现

上面的需求看起来很适合用继承+组合来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class RawIterator: public Iterator {
public:
explicit RawIterator(const vector<vector<int> >& matrix);
//...
};

class OddIterator: public Iterator {
public:
explicit OddIterator(Iterator* iter);
//...
};

class AppendPerRowIterator: public Iterator {
public:
AppendPerRowIterator(Iterator* iter, const vector<int>& appendix);
//...
};

class DoubleIterator: public Iterator {
public:
explicit DoubleIterator(Iterator* iter);
// ...
};

我们用了4个派生自Iterator的新类型来实现不同的需求,除了RawIterator外,其它3种类型都是在组合了已有的Iterator基础上实现新功能,因此可以任意顺序组合。

几个New函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Iterator* NewRawIterator(const vector<vector<int> >& matrix) {
return new RawIterator(matrix);
}

Iterator* NewOddIterator(const vector<vector<int> >& matrix) {
Iterator* rawIter = NewRawIterator(matrix);
return new OddIterator(rawIter);
}

Iterator* NewAppendPerRowIterator(const vector<vector<int> >& matrix, const vector<int>& appendix) {
Iterator* oddIter = NewOddIterator(matrix);
return new AppendPerRowIterator(oddIter, appendix);
}

Iterator* NewDoubleIterator(const vector<vector<int> >& matrix, const vector<int>& appendix) {
Iterator* appendIter = NewAppendPerRowIterator(matrix, appendix);
return new DoubleIterator(appendIter);
}

测试结果为:

1
2
3
4
Raw:164311
Odd:316020
Append:436596
Double:458733

可以看到,每增加一层功能,调用延时就明显上涨了一截。有没有其它性能更好的方案了呢?

mixin

mixin是给一组基础类,每个都建模了一个抽象概念,且能直接组合成一个新的类,像乐高一样,满足需求。如果你新增了基础类,只要它是和其它基础类正交的,就可以扩展到这个组合类里。

C++中的一种mixin方法是结合模板和继承,通过模板参数来组合基础类(见通过mixin组合功能)。

在上面这个例子中,RawIterator是一个基础类,可以单独使用,而OddIteratorAppendIteratorDoubleIterator则是用来提供新的功能的,是mixin类,我们可以将这三个类型实现为模板类,以OddIterator为例:

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
template <typename T>
class OddIterator: public T {
public:
OddIterator(?)
: T(?) {
MoveUntilOdd();
}

Node GetValue() const {
return T::GetValue();
}

bool IsValid() const {
return T::IsValid();
}

void MoveNext() {
T::MoveNext();
MoveUntilOdd();
}
private:
void MoveUntilOdd() {
while (T::IsValid() && T::GetValue().mValue % 2 == 0) {
T::MoveNext();
}
}
};

可以看到,新的OddIterator是通过继承自T来给T增加功能的,这有什么好处?摘取上文中的一段:

不需要堆分配。
没有运行期检查null。
不需要显式管理生命期。
没有多余的虚函数定义及其调用。
编译器有机会做更多优化。

似乎一切都很美好,除了代码有些怪异外。但我们还有一个问题没有解决:怎么构造T

一种mixin的通用构造方法

这是一个很大很复杂的课题,这里我只提出一种似乎可行的方式(至少本文的例子是没问题的)。

我们要求每个mixin类型,及需要与这个mixin类型组合的基础类型内部,都定义一个Context类型,且有一个只有一个Context类型参数的构造函数。例子:

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
class RawIterator: public Iterator {
public:
struct Context {
const vector<vector<int> >& mMatrix;

explicit Context(const vector<vector<int> >& matrix)
: mMatrix(matrix) {}
};

explicit RawIterator(const Context& context);
// ...
};

template <typename T>
class OddIterator: public T {
public:
struct Context {
const typename T::Context& mBase;

explicit Context(const typename T::Context& base)
: mBase(base) {}
};

OddIterator(const Context& c)
: T(c.mBase)
//...
};

template <typename T>
class AppendPerRowIterator: public T {
public:
struct Context {
const typename T::Context& mBase;
const vector<int>& mAppendix;

explicit Context(const typename T::Context& base, const vector<int>& appendix)
: mBase(base), mAppendix(appendix) {}
};
AppendPerRowIterator(const Context& c)
: T(c.mBase)
//...

通过这种方式,我们可以在不知道T的具体类型(也就不知道它到底有什么样的构造函数)的情况下,构造出组合类型。从而也获得了任意组合这些mixin顺序的能力。

在C++11后,我们可以通过变长参数和继承构造函数的方式提供更优雅的mixin的通用构造方案,这是另外的话题,这里不展开了。

在使用了上面的方法来构造mixin后,几个New函数的实现为:

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
Iterator* NewRawIterator(const vector<vector<int> >& matrix) {
RawIterator::Context c(matrix);
return new RawIterator(c);
}

Iterator* NewOddIterator(const vector<vector<int> >& matrix) {
typedef OddIterator<RawIterator> OddIter;

RawIterator::Context c0(matrix);
OddIter::Context c1(c0);
return new OddIter(c1);
}

Iterator* NewAppendPerRowIterator(const vector<vector<int> >& matrix, const vector<int>& appendix) {
typedef OddIterator<RawIterator> OddIter;
typedef AppendPerRowIterator<OddIter> AppendIter;

RawIterator::Context c0(matrix);
OddIter::Context c1(c0);
AppendIter::Context c2(c1, appendix);
return new AppendIter(c2);
}

Iterator* NewDoubleIterator(const vector<vector<int> >& matrix, const vector<int>& appendix) {
typedef OddIterator<RawIterator> OddIter;
typedef AppendPerRowIterator<OddIter> AppendIter;
typedef DoubleIterator<AppendIter> DoubleIter;

RawIterator::Context c0(matrix);
OddIter::Context c1(c0);
AppendIter::Context c2(c1, appendix);
DoubleIter::Context c3(c2);
return new DoubleIter(c3);
}

看起来还是很笨拙,但至少我们成功地把它们构造出来了。

mixin的性能

重新编译,运行,基于mixin的结果为:

1
2
3
4
Raw:165505
Odd:164377
Append:187320
Double:191260

可以看到,在Raw结果不变的情况下(这是当然的),其它三种场景mixin方案的延时分别比继承+组合减少了48%、57%和58%,调用链越长,mixin方案的优势越大。

结论

本文提出了用mixin来解决功能组合的方案,且使用了Context类型来解决构造问题,相比常见的继承+组合的方案,mixin方案有着非常明显的性能优势。

注:本文所使用编译器均为gcc4.1.2。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// file.h

#ifndef XXX_FILE_H
#define XXX_FILE_H

class File {
public:
explicit File(int content);

int GetContent();
private:
bool IsLoaded();
void Load();
private:
bool mIsLoaded;
int mContent;
};

#endif

首先我们在file.h中定义了一个类型File。为了保持头文件简洁,我们这里只声明了各个成员函数,而将定义都放到实现文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// file.cpp

#include "file.h"

File::File(int content)
: mIsLoaded(false),
mContent(content) {}

int File::GetContent() {
if (!IsLoaded()) {
Load();
}
return mContent;
}

bool File::IsLoaded() {
return mIsLoaded;
}

void File::Load() {
mIsLoaded = true;
}

接下来,我们在main.cpp中使用这个类:

1
2
3
4
5
6
7
8
9
10
11
// main.cpp

#include "file.h"
#include <iostream>

using namespace std;

int main() {
File f(5);
cout << f.GetContent() << endl;
}

在这个很简单的例子中,我们要关注的是:对于简短的成员函数,将其实现放到.cpp中,而不是直接定义在类定义体中(也就是放到.h中class File内部),会不会损害性能?

我们用-O2编译,再看一下其汇编指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0000000000400840 <_ZN4File10GetContentEv>:
400840: 53 push %rbx
400841: 48 89 fb mov %rdi,%rbx
400844: e8 d7 ff ff ff callq 400820 <_ZN4File8IsLoadedEv>
400849: 84 c0 test %al,%al
40084b: 75 08 jne 400855 <_ZN4File10GetContentEv+0x15>
40084d: 48 89 df mov %rbx,%rdi
400850: e8 db ff ff ff callq 400830 <_ZN4File4LoadEv>
400855: 8b 43 04 mov 0x4(%rbx),%eax
400858: 5b pop %rbx
400859: c3 retq
40085a: 90 nop
40085b: 90 nop
40085c: 90 nop
40085d: 90 nop
40085e: 90 nop
40085f: 90 nop

可以看到,GetContent里确实调用了IsLoadedLoad

我们知道,如果把IsLoadedLoad的定义放到类定义体内,这样的函数是默认内联的,GetContent中就不会真的去调用这两个函数。但现在,我们为了简洁,把它们的实现移到了.cpp中,导致了它们没有被内联,确实损害了性能。

怎么既把小函数定义放到.cpp中,又能让它们在GetContent中内联呢?

我们先把IsLoadedLoad的实现放到GetContent前面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool File::IsLoaded() {
return mIsLoaded;
}

void File::Load() {
mIsLoaded = true;
}

int File::GetContent() {
if (!IsLoaded()) {
Load();
}
return mContent;
}

这次编译的结果没有变,IsLoadedLoad没有内联。

我们再在两个函数的定义前面加上inline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
inline bool File::IsLoaded() {
return mIsLoaded;
}

inline void File::Load() {
mIsLoaded = true;
}

int File::GetContent() {
if (!IsLoaded()) {
Load();
}
return mContent;
}

这回编译结果变成了:

1
2
3
4
5
6
7
8
9
10
0000000000400820 <_ZN4File10GetContentEv>:
400820: 80 3f 00 cmpb $0x0,(%rdi)
400823: 75 03 jne 400828 <_ZN4File10GetContentEv+0x8>
400825: c6 07 01 movb $0x1,(%rdi)
400828: 8b 47 04 mov 0x4(%rdi),%eax
40082b: c3 retq
40082c: 90 nop
40082d: 90 nop
40082e: 90 nop
40082f: 90 nop

可以看到,inline起作用了,GetContent里面的函数调用都被抹平了。

假如把IsLoadedLoad的实现加上inline,但放到GetContet后面,inline还会生效吗:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int File::GetContent() {
if (!IsLoaded()) {
Load();
}
return mContent;
}

inline bool File::IsLoaded() {
return mIsLoaded;
}

inline void File::Load() {
mIsLoaded = true;
}

编译结果为:

1
2
3
4
5
6
7
8
9
10
0000000000400820 <_ZN4File10GetContentEv>:
400820: 80 3f 00 cmpb $0x0,(%rdi)
400823: 75 03 jne 400828 <_ZN4File10GetContentEv+0x8>
400825: c6 07 01 movb $0x1,(%rdi)
400828: 8b 47 04 mov 0x4(%rdi),%eax
40082b: c3 retq
40082c: 90 nop
40082d: 90 nop
40082e: 90 nop
40082f: 90 nop

可以看到,即使我们把待inline的成员函数的实现放到调用处后面,这种inline仍然是生效的。

结论:我们可以在成员函数的定义前显式加上inline,这样同文件的调用处就可以内联这个成员函数,去除多余的跳转,提升性能,且在同一个.cpp文件中待内联的函数与调用处的顺序不会影响内联的效果。