oohcode

$\bigodot\bigodot^H \rightarrow CODE$

中文搜索引擎coreseek的安装使用

项目中原来使用的是solr,由于solr是java写的,团队中没有java的经验,并且原来维护的人离职了,所以改用sphinx search来做项目的内部检索服务,由于中文的特殊性,我们选择了coreseek这个基于sphinx的中文搜索引擎。但是好像这个好久没人维护了~一直停留在了4.1beta版本~

下载解压

源码下载地址oreseek-3.1-beta.tar.gz;
解压下载的文件,进入目录cd coreseek-4.1-beta;

安装中文分词库mmseg

  • cd mmseg-3.2.14/
  • ./bootstrap
  • ./configure --prefix=/usr/local/mmseg3
  • make && make install

如果你幸运的话,这时候会提示错误:

1
2
3
4
 
css/SynonymsDict.cpp:94: 错误:在 {} 内将‘239’从‘int’转换为较窄的类型‘char’
css/SynonymsDict.cpp:94: 错误:在 {} 内将‘187’从‘int’转换为较窄的类型‘char’
css/SynonymsDict.cpp:94: 错误:在 {} 内将‘191’从‘int’转换为较窄的类型‘char’

可能是编译器的要求太严格导致的,只有更改源码了:
将rc/css/SynonymsDict.cpp文件94行的代码char txtHead[3] = {239,187,191};改为如下的形式
1
2
3
4
char txtHead[3] = {}; 
txtHead[0] = (char)239;
txtHead[1] = (char)187;
txtHead[2] = (char)191;

再次运行make && make install会发现又出现了错误提示:
1
2
3
4
5
 
mmseg_main.cpp: In function ‘int segment(const char*, css::Segmenter*)’:
mmseg_main.cpp:254: 错误:在 {} 内将‘239’从‘int’转换为较窄的类型‘char’
mmseg_main.cpp:254: 错误:在 {} 内将‘187’从‘int’转换为较窄的类型‘char’
mmseg_main.cpp:254: 错误:在 {} 内将‘191’从‘int’转换为较窄的类型‘char’

同样的办法修改src/mmseg_main.cpp文件的254行char txtHead[3] = {239,187,191};为:
1
2
3
4
char txtHead[3] = {}; 
txtHead[0] = (char)239;
txtHead[1] = (char)187;
txtHead[2] = (char)191;

再次运行make && make install,终于成功了~
到这里mmseg就算安装成功了

安装coreseek

  • cd csft-4.1/
  • sh buildconf.sh
  • ./configure --prefix=/usr/local/coreseek --without-unixodbc --with-mmseg --with-mmseg-includes=/usr/local/mmseg3/include/mmseg/ --with-mmseg-libs=/usr/local/mmseg3/lib/ --with-mysql
  • make && make install

测试mmseg分词,coreseek搜索(需要预先设置好字符集为zh_CN.UTF-8,确保正确显示中文)

1
2
3
4
5
$ cd testpack
$ cat var/test/test.xml #此时应该正确显示中文
$ /usr/local/mmseg3/bin/mmseg -d /usr/local/mmseg3/etc var/test/test.xml
$ /usr/local/coreseek/bin/indexer -c etc/csft.conf --all
$ /usr/local/coreseek/bin/search -c etc/csft.conf 网络搜索

上面是官方文档给的,需要说明的是,csft.conf是由sphinx.conf.dist复制而来的,里面的关于mysql的配置,自己按照实际情况修改。

C语言:可变参数函数

函数一般的参数都是固定的,但是有些时候我们需要让函数的参数是可变的,为了满足这个需求,C语言提供了库函数stdarg.h来满足要求。

可变参数参数简介

使用方法

可变参数函数的使用要求比较严谨,必须按照下面的方法进行使用:

  1. 在函数原型中使用省略号。
  2. 在函数定义中创建一个va_list类型的变量。
  3. 用宏将改变量初始化为一个参数列表。
  4. 用宏访问这个参数列表。
  5. 用宏完成清理工作。

函数原型

1
2
3
4
void f1(int n, ...); //合法
int f2(int n, const char *s, ...); //合法
char f3(char c1, ..., char c2); //无效,省略号必须是最后一个参量
doubel f3(); //无效,没有任何参量

程序举例

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
# include <stdio.h>
# include <stdarg.h>

double sum(int , ...);
void printSth(int, const char *, ...);

int main(void)
{
double s, t;

s = sum(3, 1.1, 2.5, 13.3);
t = sum(6, 1.1, 2.1, 13.1, 4.1, 5.1, 6.1);
u = sum(6, 1.1, 2.1, 13.1, 4.1, 5.1, 6.1);
printf("return value for "
"sum(3, 1.1, 2.5, 13.3): %g\n",s);
printf("return value for "
"sum(6, 1.1, 2.1, 13.1, 4.1, 5.1, 6.1) %g\n", t);

return 0;
}


double sum(int lim, ...)
{
va_list ap; //声明用户存放参数的变量
double tot = 0;
int i;
va_start(ap, lim); //把ap初始化为参数列表
for(i = 0; i < lim; i++)
tot += va_arg(ap, double); //访问参数列表中的每一个项目
va_end(ap); //清理工作
return tot;
}

程序分析

  • 具有可变参数的函数sum的第一个参数是表示一共有多少个不确定的参数,在调用的时候传递。这个参数主要是高数var_arg能够取多少次传递的参数。
  • 函数va_start()第一个参数是va_list类型的,第二个参数是这个函数确定的参数中最有一个参数,var_start()函数的作用就是把最有一个确定参数后面的所有不确定参数做一个参数列表存储到va_list类型的变量中。
  • 函数va_arg的目的是从未知参数列表中取出参数,每次调用取一个,按照参数顺序取。第一个参数是存储参数列表的变量,第二个参数标识要去的参数的类型,类型决定了要从内存栈中读取多少位。
  • 函数va_end函数主要是做清理工作,主要是释放动态分配的用于存放参数的内存。

运用

我在学了《C Primer Plus》这本书之后打算看看redis源码的时候发现redis的源码中,所有的系统log的记录都是通过这中函数来实现的:

1
2
3
4
5
6
7
8
9
10
11
12
void redisLog(int level, const char *fmt, ...) {
va_list ap;
char msg[REDIS_MAX_LOGMSG_LEN];

if ((level&0xff) < server.verbosity) return;

va_start(ap, fmt);
vsnprintf(msg, sizeof(msg), fmt, ap);
va_end(ap);

redisLogRaw(level,msg);
}

linux命令行控制字符汇总

我们经常在linux终端下输入命令行,就像我们使用vi等编辑器一样频繁,vi有很多快捷键提高我们的输入效率,命令行下有没有一些快捷键也能提高我们的输入效率呢?当然有~,下面就介绍一下这些快捷键,掌握了这些就可以让你在命令行下输入命令的效率有质的飞跃了~

linux命令行字符功能汇总

命令 名称 作用
ctl-A 回行首 使光标回到一行的行首
ctl-B 退格(非破坏性的) 向前移动光标
ctl-C break 终止当前运行前台程序
ctl-D 删除字符/退出 删除光标所在的字符, 当无字符可删除时相当于logout
ctl-F 前行 光标向前移动
ctl-G
ctl-H 退格(破坏性的) 向前移动光标的同时删除字符
ctl-I 水平制表 相当于tab键
ctl-J 重起一行 相当于回车键
ctl-K 垂直制表 删除光标所在处到行尾的所有字符
ctl-L 清屏 相当于clear命令
ctl-M 回车 相当于回车键
ctl-N 向下查找命令 对于输入过的命令,
可以通过这个命令向下查找输入过的命令
ctl-O 回车 相当于回车键
ctl-P 向上查找命令 对于输入过的命令,
可以通过这个命令向上查找输入过的命令
ctl-Q 恢复 在终端中恢复stdin
ctl-R 历史命令查找 输入ctl-R会提示查找历史命令,
然后输入字符,会时时现实匹配到的命令,
按回车就可以执行历史命令
ctl-S 挂起 在终端中冻结stdin, 于ctl+Q正好相反
ctl-T 交换 交换光标所在处的字符与光标前面的一个字符的位置
ctl-U 删除字符到行首 删除光标所在的字符到行首,于ctl+K正好方向相反
ctl-V 允许插入控制字符 如果想要输入控制字符,需要先输入ctr+V然后在输入控制字符
ctl-W 删除光标前字符到空格处 删除光标所在的字符到左边的空格处
ctl-X
ctl-Y 粘贴 把刚刚暂存区的字符粘贴到终端
ctl-Z 挂起 暂停前台作业

linux shell 编程之语法学习

shell语法跟一般类C的语法有些相识,但是却有很多独特的地方,如果不能够好好理解这些语法特性,难免在编写shell脚本的过程中会遇到很多令人难以察觉的,头疼的问题。细节决定成败,这篇博客就根据我自己的学习过程做一下总结吧。

独特的开头

一般的脚本语言都有一个基本表示自己是何方神圣的开头,比如php语言的<?php, jsp语言的<%jsp。shell也有自己独特的开头。比如#! /bin/bash, 不过跟其他语言表示自己的语言名称不一样,这里的开头表明shell要指明使用那个解释器。因为shell有很多标准,每个标准的解释器对shell的理解是不一样的,所有你写的shell脚本很可能是其他解释器不认识的内容,所以需要指明你自己使用哪个解释器。这里要注意的是如果写了#! /bin/sh 表明使用的是当前系统默认的解释器。

ps小贴士:

  1. #!一定要写在脚本第一行才能生效, 否则视为注释行, 不起任何作用
  2. 这一行是会被执行的,如果写其他命令如#! /bin/more 则会执行相应的命令

执行shell脚本的方法

shell执行的方式不同,其与运行的远离也不同,参考Shell如何执行命令这篇文章,进行介绍一下:
首先有一个脚本script.sh,内容如下:

1
2
3
4
# ! /bin/sh

cd ..
ls

执行这个脚本的方法有两种:

  • $ ./script.sh
  • $ sh ./script.sh

但其实第一种方法会转化为第二种方法,比如如果用第一种方法执行脚本,实际上会转化为:/bin/sh ./script.sh。接下来就调用shell子进程来执行脚本了,在脚本执行的所有都是针对子进程的,不会对父进程产生影响,这点可以参考博客中举的例子。

ps小贴士:

  1. 第一种方式执行脚本需要这个脚本具有可执行的权限, 第二种则不需要

变量的表示

Bash变量像一般的脚本语言一样,不区分变量类型,本质上Bash变量都是字符串。

变量替换

变量的名字就是变量保存值的地方,引用变量的值就叫变量替换。我们用$+变量名称来表示变量替换。下面几种情况变量不带$:

  • 变量被声明: variable
  • 变量被赋值: variable=12
  • 变量被unset: unset variable
  • 变量被export: export variable=23

弱引用(部分引用):双引号(“”)括起来的变量。这种引用变量替换不会被阻止。
强引用(全引用):单引号(’’)括起来的变量。替换被阻止,解释为字符串。

ps: $variable其实是${variable}的简写形式,但是有些时候简写形式不能够满足变量的更多特性(比如参数替换),这时候就要用全写形式了。

变量赋值:

变量赋值的方式也有多种:

  1. 使用赋值操作:=(注意等号两边不能有空白,否则就是视为条件判断了)赋值.a=12
  2. 使用let赋值.let a=2
  3. for循环中赋值(伪赋值).for a in \seq 100``

特殊的变量类型

  • 位置参数: 命令行运行脚本可以传递参数,而脚本接受参数可以通过位置参数来获取。
    • $0:代表脚本名称
    • $1:表示第一个传递的参数(依次类推, 但是$10开始需要需要用${10}来获取)
    • $#:表示参数的个数

条件判断

条件判断格式:

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
#  1 一般的格式
if 条件
then
...
else
...
fi

# 2 两条语句写到一行是,语句间要用分号隔开
if 条件; then
...
else
...
fi

# 3 多个条件判断
if 条件
then
...
elif 条件
...
fi

# 4 嵌套条件判断
if 条件
then
if 条件
then
...
fi
fi

条件格式:

  1. [ ... ] : [是一个内建命令,其作用就是后面的表达式退出状态码为0则条件为真。
  2. [[ ... ]]: [[是一个关键字,其作用就是和上面相同,但是其表达式的解释不同。
  3. (( ... )): 测试条件是一个算术表达式,表达式的结果为非零时条件为真。
  4. 一般命令: 可跟一般的shell命令,命令的退出状态码为测试条件。

ps: [][[]]的区别:
[是一个内建命令, 其后面跟的是一般命令的参数和选项,比如[ 1 -lt 2]可以认为1和2是参数,-lt是一个选项。但是 [ 1 && 2 ]则不正确,因为 &&不是一个有效的参数或者选项。
[[是一个关键字,可以正确解释&&, 是一个更加通用的结构。

循环

循环的方式主要有以下几种:

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
44
45
46
47
48
49
50
51
52
# 1 基本结构
for arg in [list]
do
commands...
done

# 2 使用$@(即位置参数)
for arg
do
commands...
done

# 3 使用命令替换[list]
for arg in `command`
do
commands...
done

# 4 使用C风格
for ((a=1; a <= LIMIT ; a++))
do
commands...
done

# 5 while
while [condition] #与条件判断的condition一样
do
commands...
done

# 6 until 类似于C的do...while
until [condition]
do
commands...
done

# 7 嵌套循环与控制
for a in [list]
do
for b in [list]
do
for c in [list]
do
break 2 #带层参数:退出从本层算,往外到第二层的循环
done
for d in [list]
do
continue #不带层参数:继续本层的循环
done
break #不带层参数:退出本层循环
done
done

分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# case 
case "$variable" in
$condition1 ) command...;;
$condition2 ) command...;;
...
$conditionn ) command...;;
*) command ...;; #相当于default
esac

# select 介于for和case之间的一个if语句
select variable [in list] #满足条件则执行下面的命令, 不写后面的则跟for一样,取$@
do
commands...
done

函数

函数定义的两种方式:

1
2
3
4
5
6
7
8
9
function function_name {
$1 #位置参数作为传递的参数
$2
command...
}

function_name {
command...
}

函数调用

1
function_name $arg1 $arg2

ps:

  1. 函数调用之前必须先定义

结语

到这里一个shell的基本架构有了,但是shell学习才刚刚开始,以后陆续有文章就shell的某个点进行深入探讨~

csapp chapter5:优化程序性能

编写高效程序需要几类活动:第一,我们必须选择一组合适的算法和数据结构。第二,我们必须编写出编译器能够优化以转化成高效可执行代码的源代码。本章重点讲的是第二个活动,利用我们对编译器的了解来写出让编译器编译出更高效的机器码的方法。

优化编译器的能力和局限性

程序优化的第一步就是消除不必要的内容,让代码尽可能有效地执行它期望的工作。主要包括消除不必要函数调用条件测试存储器引用
编译器本身通过复杂精细算法对程序编译时进行了优化。编译器的优化有不同的级别,也是高程度的优化程序的执行效率就越高,但同时也可能增加程序的规模,也可能使标准的调试工具更难对程序进行调试。
编译器必须很小心的对程序只是用安全的优化,也就是说对于程序可能遇到的所有可能情况,在C语言标准提供的保证之下,优化后得到的程序和未优化的版本有一样的行为。
通过一个例子来理解决定一种程序转换是否安全的难度:

1
2
3
4
5
6
7
8
9
10
void twiddle1(int *xp, int *yp)
{
*xp += *yp;
*xp += *yp;
}

void twiddle1(int *xp, int *yp)
{
*xp += 2* *yp;
}

这两个过程似乎有相同的行为。而且twiddle2的效率更高一些,因为twiddle1需要6次存储器引用,而twiddle2只需要三次存储器引用。但是当xp与yp的值相同时,twiddle1中*xp的值时原来的4倍,twiddle2却是3倍。编译器并不知道它们会不会指向同一地址,所以不能进行从twiddle1到twiddle2的优化。
这种两个指针可能指向同一存储器位置的情况称为存储器别名使用(memory aliasing)。编译器对这种情况的优化能力是有限的,所以需要写程序的过程中进行优化。

表示程序性能

引入度量标准每元素的周期数(Cycles Per Element, CPE),作为一种表示程序性能并指导我们改进代码的方法。
处理器活动的顺序是由时钟控制的,时钟提供了某个频率的规律信号,通常用千兆赫兹(GHz)。就是我们常说的CPU的运行频率。时钟周期就是时钟频率的倒数。

程序示例

举例说明程序优化的效果,这里写出最后的运行函数:

1
2
3
4
5
6
7
8
9
void combine1(vec_ptr v, data_t *dest)
{
long int i;
*dest = IDENT;
for (i=0; i < vec_length(v); i++){
data_t val;
get_vec_element(v, i, &val);
*dest = *dest OP val;
}

看一下优化钱和优化后的效果,就可以看到,CPE从平均29左右降到了12左右,效果的确很明显啊。

消除循环的低效率

上面过程在执行for循环的时候调用vec_length函数左右循环测试的条件,也就是说每次执行条件测试的时候都要执行这个函数调用过程,这明显地影响了程序的性能。注意到向量的长度并不会随程序运行发生改变,所以可以考虑只计算一次向量的长度,然后条件测试都用这个值,如下函数:

1
2
3
4
5
6
7
8
9
10
11
void combine2(vec_ptr v, data_t *dest)
{
long int i;
long int lenght = vec_length(v);
*dest = IDENT;

for (i=0; i < length; i++){
data_t val;
get_vec_element(v, i, &val);
*dest = *dest OP val;
}

再看修改后的程序,CPE降到了8左右。
这个优化是一种常见的优化例子,称为代码移动(code motion)。这类优化包括识别要执行多次但是计算结果不会改变的计算。编译器会试着进行代码移动,但是对于会改变在哪里调用函数或调用多少次的变换,编译器通常都会非常小心。如果vec_length有某种副作用,那么combine1和combine2可能有不同的行为。为了改进代码,程序员必须经常帮助编译器显式地完成代码的移动。

减少过程调用

过程调用会带来相当大的开销,而且妨碍大多数形式的程序优化。
上面的combine2中,每次循环迭代都会调用get_vec_element来获取下一个向量元素,很明显会照成低效率。对其进行优化的思路就是减少过程的调用,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
data_t *get_vec_start(vec_ptr v)
{
return v->data;
}

void combine3(vec_ptr v, data_t *dest)
{
long int i;
long int lenght = vec_length(v);
data_t *data = get_vec_start(v);

*dest = IDENT;

for (i=0; i < length; i++){
*dest = *dest OP data[i];
}
}

得到的代码运行速度开得多,这是以损害一些程序的模块性为代价的。
实际情况是,得到的性能提高出乎意料的普通,这是因为5.11讲到的分支预测策略会预计函数的返回值,预测对的情况下直接执行,预测错误的话会有更多的处罚,如果分支预测策略比较好的话,预测正确的概率就高,所以带来的性能问题就越小。

消除不必要的存储器引用

combine3过程中for循环对应的汇编代码如下:

1
2
3
4
5
6
7
.L498:
movss (%rbp), %xmm0 #从dest读取数据
mulss (%rax, %rdx, 4), %xmm0 #数据相乘
movss %xmm0, (%rbp) #存储结果到dest
addq $1, %rdx #加1
cmpq %rdx, %r12 #判断边界条件
jg .L498 # if >, 循环

上面的代码不断从(%rbp)处读取和操作数据,其实可以去掉这个中间寄存器以消除不必要的读写。可以直接利用%xmm0来保存积值。向下面这样:
1
2
3
4
5
.L488:
mulss (%rax, %rdx, 4), %xmm0 #数据相乘
addq $1, %rdx #加1
cmpq %rdx, %rbp #判断边界条件
jg .L488 # if >, 循环

对应C代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
void combine4(vec_ptr v, data_t *dest)
{
long int i;
long int lenght = vec_length(v);
data_t *data = get_vec_start(v);
data_t acc = IDENT;

for (i=0; i < length; i++){
acc = acc OP data[i];
}

*dest = acc;
}

编译器不会做出这样的优化还是因为前面提到的存储器别名使用带来的问题。例如:

1
2
combine3(v, get_vec_start(v)+2);
combine4(v, get_vec_start(v)+2);

执行这两个函数的时候结果就会出现不一致性。

理解现代处理器

上面所说的都是从程序的调用角度考虑的,主要是减少不必要的指令执行,但是并没有运用目标机器的任何特性。随着试图进一步提高性能,我们必须考虑利用处理器微体系结构的变化,也就是处理器用来执行指令的底层系统设计。在实际的处理器中,是可以同时对多条指令求值的,这个现象称为指令级并行。现代处理器取得了不起的功绩之一就是:它们采用复杂而奇异的微处理器结构,其中,多条指令可以并行地执行,同时又呈现一种简单地顺序执行的表象。
程序的最大性能在处理器中主要受到下面两个重要因素的影响:

  • 当一系列操作必须严格顺序执行时,就会遇到延迟界限(latency bound)。因为在下一条指令开始之前,这条指令必须结束。
  • 吞吐量界限(throughput bound)刻画了处理器功能但愿的原始计算能力。这个界限是程序性能的终极限制。

下面通过一些例子说明如何利用现代处理器的流水线,指令级并行等特性提高程序性能:

  • 例子1, 下面代码是对一个数据组求和,根据下面的代码可以

    1
    2
    3
    4
    5
    //combine 1
    for(i = 0; i< limit; i++)
    {
    acc = acc * data[i];
    }

    下面是这段代码对应的在系统中运行的过程:

    下面是过程的简化:

    其中寄存器xmm0是变量acc对应。可见这段代码中,制约性能的瓶颈就是每次都要进行mul运算,更新acc的值,然后进入下一个循环周期。如何提高这样的程序性能呢?这个运算已经是最简洁的了,但是可不可以考虑减少循环的次数来优化呢?请看下面的这段代码.

  • 例子2, 减少循环周期也可以这么做:
    //combine 2

    1
    2
    3
    for(i = 0; i < limit; i+=2) {
    acc = (acc * data[i]) * data[i+1];
    }

    上面这段代码看似把循环周期减少了一般,但其实性能并没有得到提高,通过他的计算过程可以看到:

    虽然周期减少了一半,但是每个周期却进行了两次运算,而且每次运算都依赖上次计算的结果,也就是acc的值,所以性能并未得到改善。再看例子3。

  • 例子3, 多变量提高并行

    1
    2
    3
    4
    5
    for(i = 0; i < limit; i+=2) 
    {
    acc0 = acc0 * data[i];
    acc1 = acc1 * data[i+1];
    }

    这段代码跟上一段代码的区别是增加了一个变量,也就是增加了对寄存器的使用,其过程如下:

    可以看到同样是减少了周期,但是每个周期的运算使用了不同的计算器,每个计算都不依赖同一周期内的其他值,利用了计算器的并行性,从而提高了程序的性能。还有其它的方式。

  • 例子4, 重新结合变换

    1
    2
    3
    for(i = 0; i < limit; i+=2) {
    acc = acc * (data[i] * data[i+1]);
    }

    这段代码跟例子2很像,只不过改变了运算结合的顺序,程序的性能却得到了很大的提高,这是为什么呢?可以看到这个程序在计算机中计算的过程如下:

    可以看到每个周期也是两个mul操作,为什么会快呢?这是因为第一个mul操作并不依赖寄存器的值,也就是说其实对于data数组之间的运算可以通过流水线并行计算,制约性能的只是acc所在的寄存器的计算,所以性能主要因素还是一个mul的操作。
    从上面可以看到利用多个寄存器可以提高性能,那是不是利用的越多越好呢?答案是:NO.因为计算机的寄存器个数是有限的,如果需要的寄存器的个超过了寄存器的个数,那就只能把这些值放入到栈中,这样就会急剧降低程序的性能。

一些限制因素

影响程序性能的限制因素有下面几个:

  • 寄存器溢出:就是上面最后一段介绍的寄存器不够用时的情况。
  • 分支预测和预测错误处罚:这回提高程序的代价。
  • 其它:如加载的性能,存储的性能等。

提高性能的技术

  1. 高级设计: 为遇到的问题选择适当的算法和数据结构。
  2. 基本编码原则:
    • 消除连续的函数调用
    • 消除不必要的存储器引用:引入临时变量来保持中间结果。只有在最后的值计算出来时,才将结果存放到数组或全局变量中。
  3. 低级优化
    • 展开循环,降低开销,并且使得进一步优化成为可能。
    • 通过使用例如多个累积变量和重新结合技术,找到方法提高指令级并行。
    • 用功能得风格重写条件操作,使得编译采用条件数据传送。

(本章完 2014-05-17 22:57:27)

csapp chapter4:处理器体系结构

现代微处理器是人类创造的最复杂的系统之一。本章简要接受处理器硬件的设计。

一个处理器支持的指令和指令的字节级编码称为它的指令集体系结构(Instruction-Set Architecture, ISA)

Y86指令集体系结构

Y86指令就是它规定的一些指令的叫法和功能。指令编码就是这些指令的字节级编码。每条指令的第一个字节表明指令的类型,分为两部分,高4位是代码(code)部分,低4位是功能(function)部分。需要操作数的指令编码更长一些,例如rmmovl %esp, 0x12345(%edx)的字节编码40*42*45230100(注意是16进制, 每一位代表4个bit位),因为rmmovl第一个字节位40; 第二个字节应该是rArB(r代表源的类型,r:寄存器,m:存储器, 这里r是寄存器;A\B代表目的类型),%esp对应的数字为4,%edx对应的数字为2,所以第二个字节是:42;,最后4字节是偏移量0x12345的编码,首先补0填充为4字节:00 01 23 45, 然后反序插入:45230100。最后连起来就是404245230100。

Y86的顺序实现

将处理组织成阶段

将处理组织成阶段主要:

  • 取指(fetch):取指阶段从存储器读取指令字节,地址为程序计数器(PC)的值。它按顺序方式计算当前指令的下一条指令的地址valP(等于PC的值加上已取出指令的长度)。
  • 译码(decode):译码阶段从寄存器文件读入最多两个操作数, 得到值valA或/和valB。
  • 执行(excute):执行阶段,算术/逻辑单元要么执行指令指明的操作,计算存储器引用的有效地址,要么增加或减少栈指针。
  • 访存(memory):访存阶段可以将数据写入存储器,或者从存储器读出数据。
  • 写回(write back):写回阶段最多可以写两个结果到寄存器文件。
  • 更新PC(PC update):将PC设置成下一条指令的地址。
    例如:
阶段 OPl rA, rB
取指 icode:ifun<-\(M_{1}\)[PC]
rA:rB<-\(M_{1}\)[PC+1]
valP<-PC+2
译码 valA<-R[rA]
valB<-R[rB]
执行 valE<-valB OP valA
Set CC
访存
写回 R[rB]<-valE
更新PC PC<-valP

SEO硬件结构

硬件单元与各个处理阶段的关联:

  • 取指:将程序计数器寄存器作为地址,指令存储器读取指令的字节。PC增加器(PC incrementer)计算valP,即增加了的程序计数器。
  • 译码:寄存器文件有两个读端口A和B,从这两个端口同事图去寄存器值valA和valB。
  • 执行:根据指令的类型,将算术/逻辑单元(ALU)用于不同的目的。
  • 访存:在执行访存操作时,数据存储器读出或写入一个存储器字。
  • 写回:寄存器文件有两个写端口。端口E用来写ALU计算出来的值,而端口M用来写从数据存储器中读出的值。

SEO的时序

SEO的实现包括组合逻辑和两种存储器设备:时钟寄存器(程序计数器和条件码寄存器), 随机访问存储器(寄存器文件、指令存储器和数据存储器)。组合逻辑不需要任何时序或控制——只要输入变化了,值就通过逻辑门网络传播。现在有四个硬件单元需要对它们的时序进行明确的控制——程序计数器、条件码寄存器、数据存储器和寄存器文件。这些单元通过一个时钟信号来控制,它触发将新值转载到寄存器以及将值写到随机访问存储器。每个时钟周期,程序计数器都会装载新的指令地址。只有在执行整数运算指令时,才会装载条件码寄存器。只有在执行rmmovl, pushl或call指令时,才会写数据存储器。根据下图来理解处理器活动的时序控制:

可以看出,其实所有的状态更新实际上同时发生,且只在时钟上升开始下一个周期时,保证这个等价性的原则是:处理器从来不需要为了完成一条指令的执行而去读由该指令更新了的状态,我对这句话的理解是,一个周期内(其实一个周期就是一条指令的执行)执行的指令所更新的数据不会成为该指令读取的数据。
上图中周期3通过组合逻辑算出了条件码的新值:000, %ebx的新值,以及程序计数器的新值(0x00e),但是这些都是一个临时状态,是组合逻辑的出口值,在下一个周期开始的时候,也就是电瓶上升沿,把这些临时的值更新到了xian相应的寄存器中,开始下一个逻辑运算。
SEO阶段的实现,就是以上每条指令逻辑运算的过程。不再说明。

流水线的通用原理

流水线化的一个重要特性就是增加了系统的吞吐量(throughput)。我理解吞吐量就是在单位时间内能够执行的命令个数。通过例子来说明:
非流水线化的计算硬件:

图中,I1,I2,I3表示的是三条指令。
一个组合逻辑需要300ps时间来进行运算,然后需要20ps的时间把数据加载到寄存器中,也就是一个延迟(latency)为320ps,所以可以计算处吞吐量:
\(吞吐量=\frac{1}{(300+20)*10^{-12}} \approx 3.12GIPS\)

流水线化的计算硬件:

图中,I1,I2,I3表示的是三条指令, A, B, C表示执行每条指令需要三个阶段。这里每个阶段为100ps, 也就是把300ps分成三次来执行。但是各个阶段之间需要放上流水线寄存器(pipeline registers),每次加载寄存器都需要20ps, 所以可以看出执行一条指令需要3*(20+100)=360ps, 比之前执行一条指令多出来40ps。但是看一下流水线的流程,A先执行I 1指令,执行完后,B开始执行I1指令,这时I2就可以进入A阶段进行执行了,最终的结果是A, B, C都在自行命令。也就是得到流水线的吞吐量是:
\(吞吐量=\frac{1}{(100+20)*10^{-12}} \approx 8.33GIPS\)
对于这三条指令可以看出,非流水线状态下一共执行了960ps, 流水线情况下执行了600ps, 提高了整个系统的执行效率。

ps: 不是流水线的级数越多约好,因为级数增加的,每个阶段的执行时间减少,吞吐量增加了,但是整个执行过程的延迟也增加了,所以收益不一定会变好。所以实际过程中要兼顾吞吐量和时延两个指标。

后面的内容说的是具体流水线的设计实现,这里就不再说了。

apue chapter2

本章主要介绍UNIX系统的标准化及不同UNIX系统的实现。

UNIX标准化

UNIX系统由各自独立的组织执行了三个标准:ISO C、IEEE POSIX以及Single UNIX Specification。

unix系统家族树

mysql merge engine 介绍

最近由于项目需求,使用了Merge Engine这个Mysql数据库引擎,看着官方文档对其了解了一下。总结加翻译一下~

MERGE引擎初体验

MERGE存储引擎又叫MRG_MyISAM存储引擎,可以把许多相同的MyISAM表可以聚集到一个表来使用。“相同”的意思是所有的表要有相同的列和相同的索引信息。
MERGE引擎的另一个代替方案是分割(partitioned)表(把一个独立的分割后的表放到一个单独的文件中)。分割表是一个比MERGE更好的方案,具体请参考第18章Partitioning的内容。
当建立一个MERGE引擎表时,会产生两个文件:.frm文件存储的是表的格式,.MRG文件包含的是这个MERGE表所包含的MyISAM表的名字(这些表可以不在同一个数据库中)。
MERGE表中可以使用 SELECT, DELETE, UPDATE, 和INSERT等数据库操作语言。前提是对每个包含的表都有这些权限。

注意:
如果一个用户有权限操作数据表t, 那么可以建立一个MERGE表m来访问t, 这时如果用户对t的权限没有了,仍然可以通过m来操作t。

如果对MERGE使用DROP TABLE那么只是删除了MERGE表,对MERGE表包含的表没有任何影响。
建立一个MERGE表的时候可以使用参数INSERT_METHOD来决定INSERT一条数据是是如何插入MERGE表所包含的表的。

INSERT_METHOD = last: 当插入一个记录时,实际插入的是union的最后一个table。
INSERT_METHOD = first: 当插入一个记录时,实际插入的是union的第一个table。
INSERT_METHOD = no: MERGE表不允许插入数据。

如果MERGE表包含的数据表结构或者个数有变化,需要重新建立MERGE表,建立一个新的映射关系,方法有下面两种:

  1. 删除这个MERGE表,重新create一个。
  2. 使用ALTER TABLE tbl_name UNION=(...), 改变所包含的表。

MERGE实现原理

由于文档没有说MERGE的内部实现原理,根据我的猜测应该是这样的,MERGE表只是记录了所包含的每个表的名字和表共同的结构,当我对表的内容进行检索时,其实MERGE是分别对它包含的每个表进行了检索,然后输出了结果,这个可以做个验证:
t是表t1,t2的MERGE表,一条记录分别在t1和t里检索

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> explain select * from t1 where a =1;
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| 1 | SIMPLE | t1 | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
1 row in set (0.00 sec)
mysql> explain select * from t where a =1;
+----+-------------+-------+------+---------------+------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+-------+------+-------+
| 1 | SIMPLE | t | ref | a | a | 4 | const | 2 | |
+----+-------------+-------+------+---------------+------+---------+-------+------+-------+

发现,对于主键进行检索时,MERGE的检索此时总是等于它包含的表的数量,而单个表进行检索时是直接在本表进行检索的。也就是说其实MERGE表只是一个聚合结构,并不含索引和数据,其操作都是在它包含的表中逐个进行的,其复杂度是单个表的和。

MERGE 优缺点

优点:

  • 对分表的管理更加简单。比如log表,可以根据时间进行分别,然后对其进行压缩,最后通过建立MERGE表来操作数据。
  • 获取更快的速度。可以把一个很大的只读表拆分为多个独立的表,放到不同的磁盘上,通过MERGE表结构来查询的速度要比查一个大表快的多。
  • 更有效的搜索。如果知道要搜索的数据在哪个表里,可以直接进行搜索,否则就可以在MERGE表中搜索,不需要对每个表都分别搜索。
  • 及时映射所有包含的表。MERGE表不需要包含索引,因为它用的是包含的表的索引。
  • 如果需要建立一个很大的表,可以见多个表然后再使用MERGE表。这样更加快而且更加节省空间(应该是索引所消耗的空间)。
  • 可以突破MyISAM表大小的限制。每个MyISAM表都有大小的限制,但是MERGE没有。
  • 对一个表也可以建立MERGE表,但是对性能并没有什么提升。only a couple of indirect calls and memcpy() calls for each read (这句话不知道如何翻译)

缺点:

  • MERGE表只能对MyISAM引擎的表建立。
  • MERGE引擎不支持MyISAM引擎的一些特性。例如不能建立FULLTEXT索引,可以在MyISAM上建立,但是不能通过MERGE表来使用。
  • 如果MERGE表不是临时的,那么它包含的表也不能是临时的,如果MERGE表是临时的,那么它包含的表可以是任何临时和不临时的表的组合。
  • MERGE表比MyISAM表需要更多的文件描述。 If 10 clients are using a MERGE table that maps to 10 tables, the server uses (10 × 10) + 10 file descriptors. (10 data file descriptors for each of the 10 clients, and 10 index file descriptors shared among the clients.),这个也不太明白。
  • 读索引比较慢。当使用索引时,MERGE需要对它包含的每个表进行查询。这让MERGE表在eq_ref搜索时很慢,但是在ref搜索时不会太慢。

MERGE 存在的问题

捡总要的说了

  • 如果改变一个原来是MERGE引擎的表为非MERGE引擎,那么MERGE表的映射就没有了,所包含的表的所有数据都会copy的修改后的表中。
  • MyISAM里的AUTO_INCREMENT字段对于MERGE来说没有用
  • MERGE不能保持唯一索引,在MyISAM中是可以的。

Jekyll 添加DOT language支持

plantuml是基于graphviz来做的,而graphviz支持DOT language,所以可不可以直接用plantuml支持DOT language来进行画图呢?这样的话岂不是更爽了~,折腾吧,骚年!

DOT language的介绍文档。在plantuml的官网找到了有关plantuml支持DOT的说明。根据这个说明的意思,开头的@startuml和结尾的@enduml需要换成@startdot@enddot就可以了,然后用plantuml.jar生产图片,官网上说只支持png格式,但是我试了一下svg也是支持的,可能是文档没有更新的原因。

但是jekyll能不能支持DOT呢?我原来想可能要改一下插件,可以在http://www.plantuml.com/plantuml/form试了一下DOT是可以生成图片的,也就是说我用的JS插件生成图片是原生就支持的。所以我直接用下面的的写法放到了jekyll里:

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

{% plantuml %}

digraph G {
subgraph cluster0 {
node [style=filled,color=white];
style=filled;
color=lightgrey;
a0 -> a1 -> a2 -> a3;
label = "process #1";
}
subgraph cluster1 {
node [style=filled];
b0 -> b1 -> b2 -> b3;
label = "process #2";
color=blue
}
start -> a0;
start -> b0;
a1 -> b3;
b2 -> a3;
a3 -> a0;
a3 -> end;
b3 -> end;
start [shape=Mdiamond];
end [shape=Msquare];
}

{% endplantuml %}


效果如下:

成功了~

jekyll添加plantuml模块

讲完了如何正常显示中文后,突然想到使用jekyll + plantuml的组合岂不是更加神奇,于是折腾之~

首先plantuml的running页面列出了plant所支持的平台及编辑器,可以看到jekyll赫然在列,所以如果按照这个教程应该没问题了,可并没有这么简单。
这个教程说的是基于octopress的,虽然octopress也是基于jekyll的,可是还是有差别的,按照教程把plugin进去,在build的时候,发现又错误提示:./plugins/raw不存在,这个包确实不存在,加上我队ruby还不熟悉,于是找这个包也找不到,折腾了半天,把行注释掉,又提示了其他的错误,加上jekyll添加plugin我也不是太清楚,所以干脆先放弃,寻找替代方案。

JS实现plantuml渲染

越是发现列表页还支持js渲染,如果能直接引用js进行渲染也是不错的选择,于是把完整包下载下来,引入default.html页面中,但是第一次并没有成功,是因为rawdeflate.js是异步加载的,必须使用http协议解析才行,而且jquery_plantuml.js的路径必须要改成你的js文件放置的路径,而且默认渲染的是图片,而我想要矢量图,所以把代码$(this).attr("src", "http://www.plantuml.com/plantuml/img/"+encode64(e.data));中的img路径改为了svg就可以了。效果如下:

1
2
3
<img uml="
Bob -> Alice : Love
">


1
2
3
4
<img uml="
Bob -> Alice: 访问
Alice --> Bob: 响应
">

发现这里有一个问题:第一个可以正常现实,第二个却不能正常现实,原因是传递uml变量的值时,正常的应该是把换行符也传递过去,但是在实际对js传递的参数进行调试的时候换行符不见了。

Plugin实现plantuml渲染

由于js生产的uml图出现了问题,不能正确解析,而且对泳道图不支持,我只有研究一下真正的plantuml plugin了。
还是接着上面的,git地址下载下来plantuml.rb_plugins/目录后,然后在blog里写出下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

{% plantuml %}

|Web|
start
:action;
|Server|
:running;
:return;
|Web|
end;

{% endplantuml %}

发现执行jekyll build时会提示不能够加载./plugins/raw, 由于对ruby不熟悉,所以找了好久也没找到这个原因,所以最后还得靠自己,我把加载这个包的代码注释了,又运行了一下build,仔细看了一下出错信息,发现里面有一个错误提示Digest这个执行有问题,发现并没有引入这个包,于是在前面加上了require "digest",再次运行发现成功了,也生成了图片地址,但是在_site目录里并没有找到这个目录,因为明明在目录里生成了,但是build完后自己又消失了,很奇怪,但是我在网上找了好久也没找到build的内部流程,所以只有另寻出路了,发现_site里面除了生成的blog目录里都是静态博客外,还有一个assets目录,里面正是根目录下的assets copy过来的,所以我想到在assets目录下建立一个plantuml生成文件的路径,果然再次build的时候可以了,但是有一个不好的点就是每次这个目录下的图片不是重新生成的,而且这个不能放到版本控制里。下面就是我的具体修改方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# _config.yml文件添加下面两行
plantuml_jar: ./_lib/plantuml.jar
plantuml_folder: /assets/img/plantuml/
# plantuml.rb 文件添加
require 'digest'

# plantuml.rb 修改生成图片的路径
# folder = "/images/plantuml/"
folder = site.config['plantuml_folder']
# folderpath = site.dest + folder
folderpath = site.dest + "/.." + folder
# filepath = site.dest + folder + filename
filepath = site.dest + "/.." +folder + filename

# plantuml.rb 修改生产svg和解决中文乱码问题
filename = Digest::MD5.hexdigest(code) + ".svg"
cmd = "java -jar " + plantuml_jar + " -tsvg -charset utf-8 -pipe > " + filepath

发现在我的电脑上build后能够正常现实,也不能正常现实,只有先把jekyll serve重启一下才能成功。但是传到git pages上却不行,我还不太清楚github pages的jekyll运行原理与本地有什么区别,该如何去控制github pages的服务器,所以这个方法还是行不通。

JS与Plugin混合渲染plantuml

上面两个方法都有问题,该如何是好呢?于是我又回到了使用js的想法,因为这个渲染后的结果是通过plantuml服务器保存的,通过仔细观察jquery_plantuml.js的内容我了解到,其实这个js是实现了一个数据压缩的算法,把传递的代码进行压缩,然后生产一个压缩后的字符串,由于字符串和代码内容是一一对应的,所以plantuml服务器就以此字符串为key对应此代码的图片(分别对应svg和png两种格式)。此前能够通过html标签传递代码,但是换行有问题,另一方面直接写html代码也不美观,可不可以把planutml.rb这个plugin改造一下,来传递代码和生成img标签呢?于是再来看plantuml.rb的代码,发现除了中间生产图片的需要用到plantuml.jar,其它的逻辑就是我们刚才想得到的东西,于是把这个插件改造如下:

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

module Jekyll

class PlantUMLFile < StaticFile
def write(dest)
true
end
end

class PlantUMLBlock < Liquid::Block
def render(context)
site = context.registers[:site]
code = @nodelist.join
source = "<center>"
source += "<img alt='this is uml image' uml='"+code+"'>"
source += "</center>"
source
end
end
end

Liquid::Template.register_tag('plantuml', Jekyll::PlantUMLBlock)

plantuml的内容还是第二种方法的内容,效果如下:
可以看到这时候plantuml的展示就正常了,而且github pages同样正常了,这时候再把第二种方法说的添加的无用目录img/plantuml/和无用文件plantuml.jar都删除就可以了~( ̄▽ ̄)

但是上传后又发现github pages不能正常显示,真是要命啊!我怀疑是不是自己的环境和github pages的环境不一样导致的,于是我又按照github pages 的官方步骤又来了一次,发现这次在本地运行提示不能找到自己定义的tag,这就对了,线上发邮件说也是这个错误。于是我又找了好久原因,原来是safe这个配置导致的,本地默认是false,可以使用第三方插件,如果为true就不能使用第三方插件,而github pages为了保证安全,默认的都是true,也就是说线上不能使用第三方插件!

网站使用静态文件

为了支持这个东西我只好再次想办法了,于是找到了一个方法,发现github pages是可以不是用jekyll而使用生成后的静态文件的,步骤如下:

  • 新建一个seanerchen.github.io.raw的repo,然后把原来seanerchen.github.io的代码copy过来,放到这个版本控制里管理。
  • 删除seanerchen.github.io的所有文件,然后touch .nojekyll这个文件。
  • copy -rf seanerchen.github.io.raw/_site/* seanerchen.github.io/
  • 然后把seanerchen.github.io 下的push上去
  • 等待缓冲实效,打开页面。可以了~

效果如下: