在这篇随笔里面提到伽罗瓦域。
参考了很多资料,才知道伽罗瓦域原来还可以用指数表作为基本表,就得到了一个乘法优化的有限域运算。但是加法恒元和乘法恒元不再是0和1。
对此表示很无奈,计算系数时候,习惯性的用0作为初始值开始求和。结果方程组怎么解都不对!折腾了半小时,才找到根源。
但是乘法优化使得加减法的代价高昂,所以对于解线性方程组,要比通常意义的伽罗瓦域还要慢。
目前在unsigned short范围内随机系数。构建一个1024元的系数矩阵,再解出,计10次平均值。
double类型使用编译器的加减乘除,需要3.8秒。
unsigned short类型使用普通伽罗瓦域,需要5.3秒。
unsigned short类型使用乘除优化的伽罗瓦域,需要6.7秒。
(Core 2 E7500 2.93GHz + 4G 1333 DDR3 @ win7 x64 MSVC2010 x86 Release Build)
最后一提,BCH编码基于伽罗瓦域运算,乘除优化对于BCH编码效率能提高很多。
这篇只是一些抱怨,没有实质内容。
实现一个文件编码(冗余)方式,各种找资料,最后使用伽罗瓦域+线性方程组。
注:代码中的类暂时不想公开,以后我觉得拿得出手的时候,会贴出来。
以上是背景。
以下是代码思路及其变化:
基础:
一个使用高斯消元法解线性方程组的Matrix类。一个有限域(伽罗瓦域)运算的类galois_field。
最初都是网上找来的代码,或存在于开源项目,或是好心人共享的代码。
拿来后阉割掉用不到的部分,全部抽象画为模板类。
最初使用一个BaseComputer类,virtual了add,min,mul,div四个纯虚函数。
galois_field继承自BaseComputer,实现了基于有限域的基本运算法则。
然后Matrix类构造函数包含一个BaseComputer指针,之后内部所有运算使用BaseComputer完成。
最后给出value_t,可以自定义内部运算使用的数据类型。如果你愿意的话,使用BigInteger,甚至Rational(有理数)也是可以的。(请自己负责数据溢出)
1 template<typename value_t>
2 class base_computer
3 {
4 public:
5 typedef value_t value_t;
6
7 virtual value_t add(const value_t &left, const value_t &right) = 0;
8 virtual value_t min(const value_t &left, const value_t &right) = 0;
9 virtual value_t mul(const value_t &left, const value_t &right) = 0;
10 virtual value_t div(const value_t &left, const value_t &right) = 0;
11 };
1 template<typename value_t, int bits, int polynomial>
2 class galois_field : public base_computer<value_t>
3 {
4 int field_size;
5 value_t *pwr_of;
6 value_t *val_of;
7
8 void build_gf();
9
10 public:
11 typedef value_t value_t;
12 galois_field();
13 galois_field(const galois_field & g);
14 ~galois_field();
15
16 value_t power_of(const value_t value) const;
17 value_t value_of(const value_t power) const;
18 int size() const;
19 int power() const;
20
21 galois_field & operator=(const galois_field & g);
22
23 virtual value_t add(const value_t &left, const value_t &right);
24 virtual value_t min(const value_t &left, const value_t &right);
25 virtual value_t mul(const value_t &left, const value_t &right);
26 virtual value_t div(const value_t &left, const value_t &right);
27 };
1 template<typename value_t>
2 class Matrix
3 {
4 public:
5 typedef value_t value_t;
6 Matrix(base_computer<value_t> *c, value_t *matrix, int rowCount, int colCount);
7 ~Matrix();
8 private:
9 int rowCount;
10 int colCount;
11 value_t* matrix;
12 value_t** matrixJump;
13 base_computer<value_t> *C;
14 int FindMaxRow(int colIndex); //选主元
15 void MulAndAdd(int srcRow, value_t k, int dstRow); //第srcRow行乘以k加到dstRow行
16 void Mul(int rowIndex, value_t k); //rowIndex行乘以k
17 void SwapRow(int srcRow, int dstRow); //交换矩阵两行
18
19 public:
20 value_t At(int row, int col) const;
21 value_t &At(int row, int col);
22 int Gasuss(); //返回0成功,其它数字表示有问题的方程个数
23 int RowCount() const;
24 int ColCount() const;
25 };
阶段性成果:
数据类型使用unsigned short, 运算器使用galois_field<unsigned short, 16, 0x1100B>(被typedef为galois_field_16)
测试64元方程组,预定解,随机64组系数,当场构建一个64x65的矩阵,交给Matrix类解出验证结果是否正确。
重复10000次,耗时42秒。
改进:
42秒真心慢,思考应该是纯虚函数的vptr跳转,以及使用方式,使得C++优于C语言的特性完全没有发挥出来。
继续改进,BaseComputer类不要了。galois_field的所有成员改成静态,Matrix的运算器也由模板管理。所有的模板,可能频繁调用的都标记inline。
最后实现一个template<typename value_t> class default_compute,用作Matrix类的默认参数。
1 template<typename value_t>
2 class default_computer
3 {
4 public:
5 typedef value_t value_t;
6
7 inline static value_t add(const value_t &left, const value_t &right){ return left + right; }
8 inline static value_t min(const value_t &left, const value_t &right){ return left - right; }
9 inline static value_t mul(const value_t &left, const value_t &right){ return left * right; }
10 inline static value_t div(const value_t &left, const value_t &right){ return left / right; }
11 };
1 template<typename value_t, int bits, int polynomial>
2 class galois_field
3 {
4 private:
5 galois_field();
6 ~galois_field();
7
8 static galois_field<value_t, bits, polynomial> gf;
9 static int field_size;
10 static value_t *pwr_of;
11 static value_t *val_of;
12
13 void build_gf();
14
15 public:
16 typedef value_t value_t;
17
18 static value_t power_of(const value_t value);
19 static value_t value_of(const value_t power);
20 static int size();
21 static int power();
22
23 static value_t add(const value_t &left, const value_t &right);
24 static value_t min(const value_t &left, const value_t &right);
25 static value_t mul(const value_t &left, const value_t &right);
26 static value_t div(const value_t &left, const value_t &right);
27 };
1 template<typename value_t, class computer_t = default_computer<value_t> >
2 class Matrix
3 {
4 public:
5 typedef value_t value_t;
6 Matrix(value_t *matrix, int rowCount, int colCount);
7 ~Matrix();
8 private:
9 int rowCount;
10 int colCount;
11 value_t* matrix;
12 value_t** matrixJump;
13 int FindMaxRow(int colIndex); //选主元
14 void MulAndAdd(int srcRow, value_t k, int dstRow); //第srcRow行乘以k加到dstRow行
15 void Mul(int rowIndex, value_t k); //rowIndex行乘以k
16 void SwapRow(int srcRow, int dstRow); //交换矩阵两行
17
18 public:
19 value_t At(int row, int col) const;
20 value_t &At(int row, int col);
21 int Gasuss(); //返回0成功,其它数字表示有问题的方程个数
22 int RowCount() const;
23 int ColCount() const;
24 };
阶段性成果:
重复上面的测试,时间变成了22秒。
此时手贱,把数据类型改成double,运算器使用default_computer。
重复上面的测试,时间是9秒。
上面的9秒是失误,不小心引入特殊数据了。
对矩阵内部计算又调整优化了一下。
以下是测试代码。
1 int main(){
2 #if 1
3 typedef galois_field_16 computer_t;
4 #else
5 typedef default_computer<double> computer_t;
6 #endif
7
8 typedef computer_t::value_t value_t;
9
10 const int n = 128;
11 const int l = 1000;
12 Matrix<value_t, computer_t> matrix(NULL, n, n + 1);
13 value_t resault[n];
14
15 int begin = clock();
16 for(int loop = 0; loop < l; ++loop)
17 {
18 for(int i = 0; i < n; ++ i) resault[i] = value_t(rand() % 65536);
19 for(int y = 0; y < n; ++ y)
20 for(int x = 0; x < n; ++ x)
21 matrix.At(y, x) = value_t(rand() % 65536);
22 for(int y = 0; y < n; ++ y)
23 {
24 matrix.At(y, n) = 0;
25 for(int x = 0; x < n; ++ x)
26 {
27 matrix.At(y, n) = computer_t::add(matrix.At(y, n), computer_t::mul(matrix.At(y, x), resault[x]));
28 }
29 }
30 matrix.Gasuss();
31 }
32 cout << clock() - begin << endl;
33 system("pause");
34 return 0;
35 }
36
就算排除掉特殊数据,结果依然是令人沮丧的。double作为运算类型,使用原本的加减乘除,要比伽罗瓦域构的计算快一倍以上。
思考:
从虚函数表改变为内联展开的模板,缩短了近一半的时间,可见当调用次数累计到可观的程度也是非常可怕的,当然,这也是C++比C高效的一部分原因。
伽罗瓦域的加减法是一样的,都是异或。那么速度一定比double快,但是乘除法就不同了。
伽罗瓦域的乘除需要3次查表,1次布尔运算,1或2次加减运算,最少需要访问5次内存。
double的话,就算是乘除法,所有操作也都是在寄存器完成的。
至此,想说的也说完了。
很想了解别人是怎么实现在有限域解线性方程组的,是更加高效呢?还是说有硬件辅助呢?希望有人指正。
首先摆出一堆零碎的想法,完全是没有经过验证的。如果有异议,欢迎指正~
这里不从头说起,如果看不明白,请自己查阅相关资料。
关于文件缓存:
借鉴Avalanche。
只是假设,一个100k的文件,被拆分成10个10k的文件b1,b2...b10。
每一次被请求都取随机系数c1,c2...c10,发送包E1=b1c1+b2c2+...+b10c10。
当一个节点收到E1,E2时候,则可以继续取得随机系数c1c2发送E3=E1c1+E2c1给其它节点。
对于下载来说,只要收集到了足够多的线性方程,通常是10个,就可以解出原始数据,降低对于稀有块的依赖
对于节点缓存,通常每个节点保持大于一的数量,以便生成新的方程。
文件分块数量应该被限制,如果分块数量过大,那么就会出现“内存不足以初始化解线性方程组的矩阵!”这种尴尬的状况。
那么对于巨型文件,不管是创建方程,还是解方程组,内存占用都是无比巨大的,所以只能对数据进行分段处理,于是依然会出现“稀缺文件段”这种p2p网络一直面对的问题。我真的不知道有什么好的方案了。
(以上所有的加法乘法基于有限域运算)
关于路由算法:
依然是借鉴,这次中枪的是骡子的kad(Kademlia)。
但是对于不稳定节点,或者是恶意节点的侦测不是很出色。(不指望有多强大的低抗攻击的能力,至少策略上能遏制一下消极上传的节点)
那么我的想法是这样~
以“邻居们”作为一个个团体,相互监视,依然是假设(数据没有经过测试),邻居取自己最近的一些节点,数量浮动在20-40之间,尽量少变更。
构建一个邻居队列,每10s(未验证)发送一个心跳(可以包含自己的工作状态,如网络压力,计算压力,存储压力等),依次发送给邻居们,那么不考虑丢包的情况,一个节点离线能很快被发现。
关于碰撞:
没有靠谱的方案。碰撞的概率本来就非常低,可以说是0概率事件。有Hash算法,把数据分配到160位的地址空间(来源于kad),要将命运交给Hash算法的优秀程度么?
就算是多级Hash,依然是在0.0000...01的概率里面继续增加零而已。没有一个靠谱的方案。但是这个问题必须解决。
所以我很无奈,那就这样好了,一旦不幸的发现了碰撞,那么给出提示“亲爱的用户,您的数据被损坏,经过我们努力,抢救到了x个可能的副本,请您选择哪个是您做需要的。。。”
恶意节点和新节点:
对于新节点,加入网络后,不会主动请求数据。所有操作都是被动的。
对于一个下载数据的节点来说,p2p网络是一个平铺的平面,那么资源就是一个以ID为圆心的圆圈,那么圆圈内的节点都“应该”有请求的数据,并且这个圆圈就是一个邻居们组成的团体。
于是,可以向圆圈的所有节点请求,如果发现某一个节点没有数据,那么表示“你是一个新来的”,好,我通知你的邻居们“这里有个新人没数据!”
于是圆圈内的节点都知道这个新人没有数据,那么就标注这个节点缺失了数据,开始等待被请求,如果长时间未被请求,那么在“邻居”对这个“新人”好感度降低。
长期来说,很多节点会做同样的工作,最终新人就会请求足够的数据,来做好自己在p2p网络中需要的存储工作。
如果说,你很消极,或者是个虚假节点,压根不工作,那么就会让邻居们非常讨厌你,给出一定的惩罚措施。比如在路由表中加入黑名单,或者封禁多长时间,长久以来的效果就是你被从网络中分割出去了。
对此,如果有人恶意攻击,不断提示某节点缺失数据,导致正常节点被分割,还没有想出靠谱的辩解方法(不能占用太多带宽)。
如果一个节点长期收不到邻居们的心跳,那么,或许你就被分割了。
如果自己是一个正常节点,那么被诬陷显然是个很大的损失,这里提供一个不靠谱的为自己辩护的方案。
当搜索到自己的邻居,但是收不到对方心跳的时候,那么就需要为自己辩护了,那么每次心跳时候,发送一点自己所缓存的数据。
作为邻居收到带有辩护数据包的时候,就增加好感度,取消封禁或者黑名单,但是显然,对于被诬陷的节点,每次心跳的时候要发送的数据会更多。至少能保证就算是个消极节点,为了呆在网络中,仍然要通过心跳把自己的数据散布到网络中。
如果存在恶意节点伪造方程组。作为下载节点,有义务分析出错误方程的来源,然后向邻居们举报该节点,降低好感度。被举报次数过多,就可以直接封禁了吧?
那么作为邻居,接受举报的先决条件是举报的节点应该是近期从本地请求过该资源的节点,所以,能稍微遏制一下恶意诬陷举报的情况,如果你想诬陷别的节点,那么你就要消耗自己的带宽先向邻居们请求数据,才可以举报某个节点。
但目前只想到这么多,欢迎提出更好的方案。
通讯方面:
全部使用UDP通讯,上层协议保证传输的稳定?如果说压根就不要“连接”这种概念会是什么效果呢?
所有的通讯都是节点对团体的,团体就是目标节点和他的邻居们。于是通讯时候。对这个团体依次发送消息,有回应,就得到数据了,没有回应,继续下一个。都没有回应的话,这个网络距离崩溃不远了。
对于目前尴尬的内网,如何搞定NAT呢?
因为节点会发送心跳,对于那些变态的网关,只用20秒就关闭udp端口,我只好对你说抱歉!那么通常心跳就足够维持自己跟邻居们的通讯了。
但是如何连接到内网用户呢?要为了现在的局面而下很大功夫去通过处在公网的邻居搞定反向连接?甚至两个内网用户再打洞?处在公网的节点压力有多大呢?一切没有经过测试,也没有找到存在的数据来分析一下压力,于是这个想法也搁浅了。
其它方面想到了,再补充。
起先家里若干电脑传输太慢,拷贝个游戏,电影,龟速啊。。。
萌生弄个千兆交换机。
若干天后,购买之~
结果发现板载网卡不是千兆的- -~
若干天后,购买之~
结果发现网线不合格,继续买新的- -~
若干天后,购买之~
now,布线也没地方藏,甩在地上,然后看着50多m/s的速度,果然好嗨~
谨此,网线不合格太坑人!
任务拖了,赶进度,晚上懒得回家了(没人),遂决定晚上不走了,能争取到上班下班路上的两个小时。
睡袋拉索打开,铺地上。
旁边还有个电风扇,搬过来打开自然风。
枕头是网购的,拿来放在睡袋上。
上闹表,关灯,OK,开始ZZZ。
好吧,我承认,一切都是美好的,要是没有蚊子的话。。。
持续挠痒痒中~
