第二十一章 高质量c/c++编程
在将算法从MATLAB或者python转换到c/c++时,需要针对平台架构优化算法资源消耗,以满足算法处理的各种要求。 不同的架构针对硬件特性最优代码可能存在差异。
处理器特性
推荐的代码风格
完整版参考谷歌c/c++代码风格。
命名规则
- 名称应当具有意义描述性,尽量避免缩略写;
- 尽人皆知的缩写可以使用;
文件名称规则
- 全小写,可以包括下划线_;
- c++文件以.cc结尾;
- 头文件以.h结尾;
- 头文件名不要和/usr/include目录下的.h重名;
- inline函数应当实现在.h文件中;
如应当用http_server_logs.h而不是logs.h命名方式。
类类型(classes, structs, type aliases, enums, 以及模板参数)
- 以大写字母开始,每一个新的单词以大写字母开始无下划线
如MyExcitingClass,MyExcitingEnum。
变量名(包括函数参数和数据成员)
- 全小写,单词之间用下划线分割,类的数据成员通常有下划线结尾
常量名
- 用k字符开始,如 const int kDaysInAWeek=7;
函数名
正常的用类似于类类型的大小写混合,存取和原子操作等较为短小意义明确的用小写. e.g. AddTableEntry() DeleteUrl() OpenFileOrDie() accessors e.g. count() and void set_count(int count).命名空间
- 全小写,最顶层命名空间基于工程名定;
枚举类型命名
- 其命名要么像常量,要么像宏,如:
宏
- #define ROUND(x) ...
- #define PI_ROUNDED 3.0
注释
- 要么都使用 // ,要么都使用 /**/
- //更为常用,建议使用//.
一行代码的长度
- 字符长度应该小于等于80个;
空格和tab键
- 使用空格,以2个空格为对其;
- 不要使用tab键;
存储架构
- 在性能和成本之间均衡
- 利用局部性原理
- 将大部分的数据访问能利用上局部性原理;
局部性原理
- 空间局部性原理,当前被存取的地址,其地址附近有接下来需要的数据;
- 时间局部性原理,当前被存取的数据,很可能不就又要被存取; 将相关的数据放在一起以便高效率访问(cache lines).
- 使用vector std::vector 是cache友好型代码,元素是连续存储在内存中的,比 std::list效率高。
c/c++ tips
编写cache友好型代码
- 一次存取一个cache line(通常是64或者32字节);
- c/c++代码按行顺序取数据,如 在内存中的排列方式是1,2,3,4; for(int i=0; i<row; ++i){ for(int j=0; j<col; ++j) { tmp_result = array[i][j]; //better for cache load tmp_result = array[j][i]; } }
- matlab是列优先;
- 避免无法预测的分支,以便分支预测成功减小pipe-line被打断的次数;
何时使用list和vector
vector连续存取元素,而list使用指针指向前后元素; 对于List - Each element takes 2 integers to point previous and next elements, so most commonly, that's 8 bytes more for each elements in your list
- Insert is linear in time:O(n)
- Remove is a constant operation:O(1)
- Access the x element is linear in time:O(n) 对于vector
- Needs less memory(no pointers to other elements, it's a simple math algorithm)
- Remove is linear in time:O(n)
- Access the x elements is constant:O(1)
- Insert can be in linear time, but most commonly, it's a constant operation:O(1)
List适合需要频繁增删元素的场景,而vector的取效率要高于list。
对于基本运算
- 尽可能使用
switch
代替if...else if ... else if ...
- 如可能,使用<<和>>代替整数的乘法和除法操作;
- 对于类对象操作优先使用+=, -= *=以及/=;
- 对于基本的数据类型操作,优先使用 +, -, *和/操作;
++运算符
- 对于类对象,使用前缀操作符
(++obj)
而不是后置操作符(obj++)
,后置操作符会将对象临时拷贝一份; x = array[i++]
比x=array[++i]
效率高;因为先计算地址需要需要两个时钟周期;- a = ++b效率比a = b++高,编译器会认为前者中a,b的值是一样的而进行优化;
初始化
- 避免无效的初始化,如果需要,使用
memset()
函数定点,浮点
- 现在cpu定点和浮点的吞吐量基本一致;
- 在64bit处理器上,64bit数学计算并不慢;
引用&和指针*
都占用空间,传输数据时有差异: - 数据结构struct和算法实现中,建议用指针*;
- 函数参数和返回值,建议用引用;
分支预测(指令流水)
- likely/unlikely
likely,是通知编译器if (true)被执行的概率比较高;
unlikely,是通知编译器if(false)被执行的概率比较高;
if (unlikely(value)){ //do thing1 }else{ // do thing2 }
如果可能,使用c实现算法
c++一些特性牺牲了一些性能,如虚方法,构造函数,每次调用通过跳转表进行跳转,c++代码size通常比c的要大,这对cache 也不够友好;
Unroll loop
double energy(
const float *data,
int dataSize
)
{
int i, dataSize4;
double result;
/* 4x unrolled loop */
result = 0.0f;
dataSize4 = dataSize & 0xFFFC;
for( i = 0; i < dataSize4; i += 4 ) {
result += data[ i + 0 ] * data[ i + 0 ] +
data[ i + 1 ] * data[ i + 1 ] +
data[ i + 2 ] * data[ i + 2 ] +
data[ i + 3 ] * data[ i + 3 ];
}
/* add any remaining products */
for( ; i < dataSize; i++ ) {
result += data[ i ] * data[ i ];
}
return result;
}
cortex-A上:
cortex-M上:
定点化
由于成本,功耗等音素,一些DSP和MCU上并没有硬浮点支持,采用软浮点较为耗时,更有甚至不支持浮点计算。 在这些处理器上要实现相关算法,浮点的计算转换到定点域里计算必不可少。
Q格式
```Q```格式的数据是名义上的定点数,它们的存储和计算均按照整数方式进行计算,这样的化就可以使用标准的整数ALU来实现浮点计算。而编程这必须确定整数和小数部分的位数以符合使用场景需要的动态范围和精度。
##浮点转成Q格式
从浮点转成```Qm.n```格式如下:
1.将浮点数乘以2^n
2.四舍五入到整数
##Q格式转成浮点
1. 乘以2^{-n}
##线程编程
本篇所述线程是指POSIX标准的线程,
>pthread库不是Linux系统默认的库,链接程序时需要使用静态库libpthread.a,编译时可以加上-lpthread参数.
在算法工程化这一章节中就提到了并发程序设计思想,并发程序设计的核心思想是利用处理多核特性,让数据处理并发,并发可以使用多进程实现也可以用使用多线程方式实现,在同一个进程中的多个线程可以访问所属进程的资源,如文件描述符,内存等;在多个线程里共享资源时就需要关注资源的一致性(信号,锁,同步)问题.
考虑时下一个比较常见的应用场景,麦克风阵列在做AEC(自动回声消除时),每一路麦克风数据处理极其相似,一路一路处理这种串行处理方式是一种方式,另外还有一种方法是设置多个线程,将每一路的语音数据分配到一个核上,并发执行,这样在四核四路麦克的情况下,实际的处理时间就是一路处理时间,这样系统的实时性就会很好.
当然在单核的情况下更需要多线程设计,举个例子,一个进程执行了定期获取麦克风寻向结果,然后根据获取到的结果进行点灯刷新操作,如果是多线程设计,那么在等待获取寻向结果线程时,这个线程是可以进入休眠的,将cpu让出来给其它线程使用,尽量充分利用cpu资源.
###线程创建
```c
#include <phtread.h>
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t *restrict attr,
void *(*start_rtn)(void*), void *restrict arg);
正确返回0,否则返回错误码.tidp是线程ID指针,attr是线程属性,新创建的线程从start_rtn开始执行,arg是传递给该函数的参数. 当线程被创建时,先运行新创建的线程还是创建线程的线程.
线程退出
处在进程中的任何一个线程调用exit
,_Exit
,_exit
将导致整个进程结束.线程可以使用三种方法退出:
- 从线程入口函数
`start_rtn
退出,返回值是线程退出值. - 线程可以被其它线程结束.
- 调用
pthread_exit
#include <pthread.h>
void pthread_exit(void *rval_ptr)
参数rval_ptr
可以被其它线程使用pthread_join
函数访问.
#include <phtread.h>
int pthread_join(pthread_t thread, void **rval_ptr)
调用pthread_join
函数的线程会被阻塞,直到其参数thread指定的线程调用pthread_exit
,从start_rtn
返回或被取消.
线程同步
互斥锁
保护数据一个时刻只能有一个线程访问.互斥锁的类型是pthread_mutex_t
,如果是动态申请内存存储互斥锁,则需要在是否内存前先调用pthread_mutex_destory
销毁互斥锁.
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
thre const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destory(pthread_mutex_t *mutex);
正确返回0,错误返回错误码.创建默认熟悉的互斥锁时将attr
设置成NULL
.
调用pthread_mutex_lock
锁住一个互斥锁,如果互斥锁已经被锁住,则调用线程被阻塞直到互斥锁被解锁.调用pthread_mutex_unlock
解锁.
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_trylock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
如果线程不能被阻塞,则可以调用pthread_mutex_trylock
有条件的给互斥锁上锁.如果锁可以上,则该函数上锁并返回0,如果锁已经被锁住,则该函数返回EBUSY
而不会阻塞线程.
读写锁
读写锁和互斥锁类似,但是提供比互斥锁更高维度的并发性.读写锁有三个状态:读情况上锁,写情况上锁和未上锁状态.当读写锁处于读锁定状态,所有其它尝试以读的方式上锁操作将被允许,任何尝试以写方式获得该锁时需要所有获得读锁的线程释放读锁.为了防止写者饥饿,通常在一个线程已经获得读锁,另一个线程处于写锁申请时,若再有一个线程尝试获取读锁,这种情况获取读锁的线程会被阻塞. 读写锁适合读的频次大于写频次的数据的保护,和互斥锁类似,读写的操作如下:
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destory(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
条件变量
条件变量是线程同步的另一个方法.条件变量本身由互斥锁保护,线程必须先锁定互斥锁才能改变条件变量的状态,其它线程只有在获得互斥锁之后才会知道锁的状态改变,条件变量(pthread_cond_t
)在使用前必须先初始化,有两种初始化方法,静态方法给条件变量赋值PTHREAD_COND_INITIALIZER
,如果条件变量是动态分配的,使用pthread_cond_init
函数进行初始化.在释放条件变量所占用的内存前,使用pthread_cond_destory
销毁条件变量.
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
pthread_condattr_t *restrict attr);
int pthread_cond_destory(pthread_cond_t * cond);
正确返回值是0,错误情况返回错误码.attr
设置成NULL
表示使用默认属性.
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timewait(pthread_cond_t *restrict cnd, pthread_mutex_t *restrict mutex,
const struct timespec *restrict timeout);
传递给pthread_cond_wait
函数的mutex
用于保护互斥锁,
线程首先锁定``mutex,这里的互斥锁用于保证
pthread_cond_wait能够并发,然后将调用线程置于等待该条件变量的链表上并解锁(以让其它线程可以被添加到等待条件变量的链表上),当
pthread_cond_wait返回时,互斥锁将被再次锁定.
pthread_cond_timewait函数在
pthread_cond_wait```函数基础上添加了超时功能,timeout设定了线程等待时间.
使用条件变量和互斥锁的实例
#include <pthread.h>
struct msg{
struct msg *m_next;
/*... more stuff here...*/
};
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
void process_msg(void)
{
struct msg *mp;
for(;;){
pthread_mutex_lock(&qlock);
while(workq == NULL);
pthread_cond_wait(&qready, &qlock);
mp = workq;
workq = mp->m_next;
pthread_mutex_unlock(&qlock);
/*now process the message mp*/
}
}
void enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}
MATLAB
基本上时频域,窗函数,滤波器,自适应滤波器等信号处理的都有,;
matlab 保存成c头文件
fid = fopen\('wave\_data.txt', 'wt'\);
fprintf\(fid, '%g,', Y\);
fclose\(fid\);
ffmpeg wav转pcm
ffmpeg -i bftest3-00.wav -f s16le -ar 16000 -ac 1 -acodec pcm\_f32le bftest3-01.raw
重采样
ffmpeg -i sp02_babble_sn01.wav -ar 16000 sp02_babble_sn01_16k.wav
python
shell字串获取和求值
获取第二列
cat log.txt | awk '{print $2}'
#!/bin/bash
cat log.txt | while read line #filename 为需要读取的文件名,也可以放在命令行参数里。
do
awk '{print $2}'
done
求第二列的最大值
cat log.txt | awk 'BEGIN {max = 0} {if ($2>max) max=$2 fi} END {print "Max=", max}'