样本熵(SampEn)的C/C++代码实现与优化
正文
本文不介绍什么是样本熵,具体推荐看此文https://blog.csdn.net/Cratial/article/details/79742363,写的很好,里面的示例也被我拿来测试代码写的对不对。
本文所有代码可以在此处找到https://gitcode.net/PeaZomboss/miscellaneous,文件夹231424-sampen。
前言
一开始有人找我帮忙写些C++代码,实现一些算法,这其中呢就有一个是样本熵。
当时粗写了一个,后来似乎性能不太够,当然实际上性能是没问题的,虽然确实不如现在优化的,但也不会很差,原因是代码被改了一部分,当然这个和算法的优化没关系,就不展开了。
不过既然都要优化了,那不如在算法层面也做一些改进,让速度再快一点,于是就想了想办法提升了性能。于是在此之后就整理了一下代码,并严谨测试,然后与大家分享。
开头提到的那篇文章里有用python写的代码,还有一个matlab写的。但是不管是python还是matlab都不是拿来实际应用的,一般追求性能的模块会用C/C++,当然用Java这类的应该也不会太差。
代码实现
首先是一般情况下很容易想到的代码:
static double step(double *X, int N, int m, double r)
{
double sum = 0;
for (int i = 0; i <= N - m; i++) {
int Bi = 0;
for (int j = 0; j <= N - m; j++) {
if (i != j) {
/* 找出最大差值 */
double D = fabs(X[i] - X[j]);
for (int k = 1; k < m; k++) {
double t = fabs(X[i + k] - X[j + k]);
if (D < t)
D = t;
}
if (D <= r)
Bi++;
}
}
sum += 1.0 * Bi / (N - m); // 会有累加误差
}
return sum / (N - m + 1);
}
double SampEn(double *X, int N, int m, double r)
{
double B = step(X, N, m, r);
if (B == 0) // 尽管大部分时候不会是0
return 0;
double A = step(X, N, m + 1, r);
if (A == 0)
return 0;
return -log(A / B);
}
这里的所有步骤基本都是按照原算法描述的内容进行编写的,所以非常好理解啊。而且我已经故意避开了诸如fmax之类的函数,优化找最大差值的逻辑,都是为了提高性能。
不过这个step函数的sum += 1.0 * Bi / (N - m);
在N较大的时候累加误差会增加,而实际上这个Bi变量完全可以累加到最后在进行运算,不但减少误差还能降低运算量,所以可以改成:
static double step(double *X, int N, int m, double r)
{
int Bi = 0;
for (int i = 0; i <= N - m; i++) {
for (int j = 0; j <= N - m; j++) {
if (i != j) {
double D = fabs(X[i] - X[j]);
for (int k = 1; k < m; k++) {
double t = fabs(X[i + k] - X[j + k]);
if (D < t)
D = t;
}
if (D <= r)
Bi++;
}
}
}
return 1.0 * Bi / (N - m) / (N - m + 1);
}
这样子几乎就是按照原来的算法思路进行了一些简单处理,但提升并不明显。
代码优化
仔细看这个step()函数,那复杂度可是O(n^2)啊,而且还要跑两次,那损耗肯定惊人。但是仔细看这两次计算,只有第一次的m变成了m+1,那能不能把两次运算整合到一起呢?
注意到m+1和m的区别在于循环次数少了一次,寻找最大差值的向量多了一个,其余是没有区别的,那我们完全可以在处理m的时候顺便把m+1的情况也放一起就行了。
请看:
double FastSampEn(double *X, int N, int m, double r)
{
int Ai = 0, Bi = 0;
int LoopsSub1 = N - m; // 循环次数减一,因为算法中的表述是从1到N-m+1,而我们是从0开始的
for (int i = 0; i <= LoopsSub1; i++) {
for (int j = 0; j <= LoopsSub1; j++) {
if (i != j) {
// 这里一样找m个的最大差值
double D = fabs(X[i] - X[j]);
for (int k = 1; k < m; k++) {
double t = fabs(X[i + k] - X[j + k]);
if (D < t)
D = t;
}
if (D <= r)
Bi++;
// 对于m+1的情况,当到达m维数的边界的时候显然是不行的
// 所以我们只要限制边界情况就行了
if (i != LoopsSub1 && j != LoopsSub1) {
double t = fabs(X[i + m] - X[j + m]);
if (D < t) // 判断最后一个是不是最大值
D = t;
if (D <= r)
Ai++;
}
} // i!=j
} // j
} // i
double B = 1.0 * Bi / (N - m) / (N - m + 1);
double A = 1.0 * Ai / (N - m - 1) / (N - m);
if (B == 0 || A == 0)
return 0;
return -log(A / B);
}
这样修改以后速度可以达到原先的1.5倍,算上循环内部的开销,这样的情况也是挺不错的。
至此,针对样本熵代码的基本优化就到这了。当然一定还会有更好的方案,比如暂存计算的结果,因为有一半是可以复用的,或者别的办法,不过这样空间复杂度会比较高,而目前的实现几乎没有额外的内存开销。同时碍于水平有限,目前只能如此。
不过,考虑到实际应用情况,这个m大多数时候都是取值2,所以针对m为2的情况又专门优化了一下:
double FastSampEn_m2(double *X, int N, double r)
{
int Ai = 0, Bi = 0;
int LoopsSub1 = N - 2;
for (int i = 0; i <= LoopsSub1; i++) {
for (int j = 0; j <= LoopsSub1; j++) {
if (i != j) {
double D = fabs(X[i] - X[j]);
double t = fabs(X[i + 1] - X[j + 1]);
if (D < t)
D = t;
if (D <= r)
Bi++;
if (i != LoopsSub1 && j != LoopsSub1) {
double t = fabs(X[i + 2] - X[j + 2]);
if (D < t)
D = t;
if (D <= r)
Ai++;
}
}
}
}
double B = 1.0 * Bi / (N - 2) / (N - 1);
double A = 1.0 * Ai / (N - 3) / (N - 2);
if (B == 0 || A == 0)
return 0;
return -log(A / B);
}
这样算是进一步压榨了CPU。
测试代码
接下来为了测试上述代码是否正确,就用开头提到的那篇文章的数据进行测试看看:
std::vector<double> x;
for (int i = 0; i < 17; i++) {
x.push_back(85);
x.push_back(80);
x.push_back(89);
}
std::cout << SampEn(x.data(), x.size(), 2, 3) << '\n';
std::cout << FastSampEn(x.data(), x.size(), 2, 3) << '\n';
std::cout << FastSampEn_m2(x.data(), x.size(), 3) << '\n';
原文给出的结果是0.0008507018803128114,不过由于std::cout的舍入问题,实际输出的是0.000850702,是符合结果的。
然后再测试一下性能:
for (int i = 0; i < 10000; i++) {
x.push_back(85);
x.push_back(80);
x.push_back(89);
}
double se;
clock_t t;
t = clock();
se = SampEn(x.data(), x.size(), 2, 3);
t = clock() - t;
std::cout << se << ", time = " << t << " ms\n";
t = clock();
se = FastSampEn(x.data(), x.size(), 2, 3);
t = clock() - t;
std::cout << se << ", time = " << t << " ms\n";
t = clock();
se = FastSampEn_m2(x.data(), x.size(), 3);
t = clock() - t;
std::cout << se << ", time = " << t << " ms\n";
先是塞了30000个数据,然后用clock()函数计时,依次测试每一个函数,然后看看结果是否一致,时间差距有多少。
如果有兴趣的话,可以去用之前提到那篇文章的python代码跑跑看,30000个数据花了我半个多小时才跑完,而我的这个代码即使没经过优化也不超过一分钟。而matlab的代码我没试过,电脑上没这个软件,也不是学这个的。
完整代码在开头给出的仓库里,打开test.cpp就可以看到。
运行测试
编译器和版本:g++ (x86_64-win32-seh-rev0, Built by MinGW-W64 project) 8.5.0
测试机器CPU:AMD Ryzen 5 4600H
测试操作系统:Windows 11 21H2
- 编译命令
g++ -D ALL_IN_ONE test.cpp -o test_x64_O0 -O0
0.000850702
0.000850702
0.000850702
2.21505e-09, time = 15275 ms
2.21505e-09, time = 9409 ms
2.21505e-09, time = 8020 ms
- 编译命令
g++ -D ALL_IN_ONE test.cpp -o test_x64_O1 -O1
0.000850702
0.000850702
0.000850702
2.21505e-09, time = 2512 ms
2.21505e-09, time = 1604 ms
2.21505e-09, time = 1151 ms
- 编译命令
g++ -D ALL_IN_ONE test.cpp -o test_x64_O2 -O2
0.000850702
0.000850702
0.000850702
2.21505e-09, time = 2654 ms
2.21505e-09, time = 1604 ms
2.21505e-09, time = 1143 ms
- 编译命令
g++ -D ALL_IN_ONE test.cpp -o test_x64_O3 -O3
0.000850702
0.000850702
0.000850702
2.21505e-09, time = 2656 ms
2.21505e-09, time = 1603 ms
2.21505e-09, time = 1145 ms
- 编译命令
g++ -D ALL_IN_ONE test.cpp -o test_x64_Os -Os
0.000850702
0.000850702
0.000850702
2.21505e-09, time = 3058 ms
2.21505e-09, time = 1631 ms
2.21505e-09, time = 1167 ms
可以看到开了-O1
及以后差距已经不明显了,说明后面的两段代码对编译器是比较友好的。
32位的就不贴了,性能肯定是不如64位的,但是依然可以看到性能提升也是非常显著的。
不过32位有个特殊的情况,就是用-Os
优化的性能居然比-O3
优化的高出一截,这点是挺有意思的,因为不管开哪个优化32位默认都是用x87 FPU进行运算的。
如果编译32位时给编译器加上-msse2 -mfpmath=sse
开关之后,不论开-Os
还是-O3
性能都是差不多的了,而且和性能64位也是基本上一样的了,毕竟64位默认就用的SSE2 FPU。
以上都是用g++测试的,如果用vs的话结果也是差不多的。
编译方案
对于x86来说,使用SSE2 FPU肯定是最好的,因为上述代码涉及大量double类型浮点数的运算,所以可以获得最佳性能。至于说什么CPU支持SSE2,那就这么说吧,20年前(以2023年为基准)的新CPU大部分都支持,因为SSE2是Intel在奔腾4(2000年)加入的,而AMD也在K8(2003年)加入了。因此可以在开编译器优化的基础上选择让编译器生成SSE2的代码。至于具体怎么操作,各家编译器有自己的方法,这个去搜一下就行了。
而对于x64来说,默认就会用SSE2,所以只要开编译器优化就行了。
如果用vs的话似乎x86默认就是开启SSE2的,而且也不用管优化,直接Release模式就行了。
更新记录
- 2023-02-04:修正错别字,优化部分表述。
最新文章
- [git] warning: LF will be replaced by CRLF | fatal: CRLF would be replaced by LF
- javascript 字符串多行的写法
- AES加密时的 java.security.InvalidKeyException: Illegal key size 异常
- HTML5游戏实战(4): 20行代码实现FlappyBird
- jquery网页字体变大小
- OpenOffice的安装与启动2
- Xcode7连接网络设置
- SQL 无法打开物理文件 XXX.mdf";,操作系统错误 5:";5(拒绝访问。)";
- Sublime Text 2 新建C++ build system
- 在web浏览器中判断app是否安装并直接打开
- html5中cookie介绍,封装以及添加,获取,删除
- VMware中Linux系统时间与主机同步以及时区设置
- [Micropython]发光二极管制作炫彩跑马灯
- 关于wordpress升级遇到的问题
- Dalvik和ART
- Hive+Sqoop+Mysql整合
- Powerdesigner设计表生成SQL脚本(带有注释)
- about Version Control(版本控制)
- AutoMapper在MVC中的运用05-映射中的忽略、处理null、多种映射转换
- ASP.NET MVC 控制器通过继承控制器来达到 过滤 并且多了一个IAuthenticationFilter
热门文章
- 16、有n个正数,使得前面每个数依次后移m个位置,最后m个数变成最前面m个数
- 【云原生 · Kubernetes】部署Kubernetes集群
- 关于mysql远程连接失败的问题解决
- ArcObjects SDK开发 005 ArcObjects SDK中的插件式架构
- 【每日一题】【回溯】2021年12月29日-93. 复原 IP 地址
- Cookie添加方法
- Flask框架(flask-sqlalchemy操作,Migrate作用,Flask迁移数据库,Flaks同步表数据)
- Burp Suite安装
- week_9(推荐系统)
- 第一篇:前端基础之HTML