目录
前言
在编写代码的时候,一个程序的运行速度是十分重要的,如果速度过慢就会变得非常鸡肋。所以就需要时间优化.
1、把后置加加/减减替换为前置加加/减减
原理解析
首先,我们应该了解两个自增/自减符号的运算原理:
i++:
#include <stdio.h>
int main(){
int a = 114514 ;
int b = 0 ;
b = a ++ ;
printf("%d %d",a,b) ;//114515,114514
return 0 ;
}
如果运行上面的代码,你就会发现,a的值比b要多1,那么也可以说b获取的是a的旧值,在这之后,a更新了新值(++操作),那么上面的代码实际上可以表达为如下:
#include <stdio.h>
int main(){
int a = 114514 ;
int b = 0 ;
int t = a ;
++ a ;
b = t ;
printf("%d %d",a,b) ;//114515,114514
return 0 ;
}
当运行代码时,你就会发现这两个程序的结果是一样的,那么我们就可知,前置加加的原理就是先返回旧值,再进行加加操作,下面的代码更加可以印证这一结论:
#include <stdio.h>
int main(){
int a = 0 ;
int b = 0 ;
b += (a ++) + (a ++) ;
/*相当于
int t = a ; ++ a ;
int t2 = a ; ++ a ;
b += t + t2 ;
*/
printf("%d %d",a,b) ;//2 1
return 0 ;
}
测试结果
所以我们可以得出,前置加加要比后置加加多创建、返回一个临时变量,哪怕它很少、很小,但是哪怕如此,积少成多也可以严重拖慢程序运行速度(如果这个变量的类型很大的话,那速度更是会被严重降低),例如下面两个程序:
#include <stdio.h>
const int N = 100000 ;
class abc{
public : //实现一个长度不可变的环
long long data[N] ;//保存长度为N的longlong的元素环
int now ; //当前环元素的下标
abc(void){ now = 0 ; }
abc(abc& b){//拷贝构造函数
//每次创建一个新的对象
//都要耗费N个longlong(data)和一个int(now)的内存
for(int i = 0;i < N;++ i)
data[i] = b.data[i] ; //遍历复制
now = b.now ; //获取下标
}
bool is_begin(){ return !now ; } // 判断是否到了环的首部
bool is_end(){ return (now + 1) >= N ;} //判断是否到了环的尾部
abc& operator++(){ (++ now) %= N ; return *this ; } //使用下一个环元素
abc operator++(int){ abc b(*this) ; (++ now) %= N ; return b ;}
//用临时对象保存下原来的值返回,
//并且使用下一个环元素
long long& operator*(){ return data[now]; } //返回环元素的引用
} ;
int main(){
abc a ;//构造一个abc类的对象a
while(!a.is_end())
*a = 100 , a ++ ;//改动在这!!!!!!!!!!!!!!!!!!
return 0 ;
}
#include <stdio.h>
const int N = 100000 ;
class abc{
public : //实现一个长度不可变的环
long long data[N] ;//保存长度为N的longlong的元素环
int now ; //当前环元素的下标
abc(void){ now = 0 ; }
abc(abc& b){//拷贝构造函数
//每次创建一个新的对象
//都要耗费N个longlong(data)和一个int(now)的内存
for(int i = 0;i < N;++ i)
data[i] = b.data[i] ; //遍历复制
now = b.now ; //获取下标
}
bool is_begin(){ return !now ; } // 判断是否到了环的首部
bool is_end(){ return (now + 1) >= N ;} //判断是否到了环的尾部
abc& operator++(){ (++ now) %= N ; return *this ; } //使用下一个环元素
abc operator++(int){ abc b(*this) ; (++ now) %= N ; return b ;}
//用临时对象保存下原来的值返回,
//并且使用下一个环元素
long long& operator*(){ return data[now]; } //返回环元素的引用
} ;
int main(){
abc a ;//构造一个abc类的对象a
while(!a.is_end())
*a = 100 , ++ a ;//改动在这!!!!!!!!!!!!!!!!!!
return 0 ;
}
乍一看,是不是感觉它们两个代码一模一样?可当我们看到main主函数while循环中,就可以发现微小的区别,在遍历a时,一个使用了a++,一个使用了++a,再让我们看看他们的速度比较(顺序相同):
天哪,仅仅是半行代码之差,运行的效率就天差地别!!!运行的时间竟然相差了百倍不止,几乎千倍!!!所以,注重代码的细节是很重要的!!!!!
2、将反复使用的数据储存为全局变量
解释
需要一直使用的资源、图片、数据等,可以一直保存在内存中,不要重复加载,这是一个十分浪费时间的操作,要尽量避免。
测试
例如以下的程序,明明使用的永远都是pow10(8),一直都是同样的数据,那么就可以用变量保存起来,而不需要每次都进行计算。
#include <stdio.h>
int pow10(int n){
int t = 1 ;
while(n) -- n , t *= 10 ;
return t ;
}
int a ;
int main(){
//int t = pow10(8) ;
while(a != -1){
scanf("%d",&a) ;
printf("它的千万位:%d\n",a / pow10(8) % 10) ;
//printf("它的千万位:%d\n",a / t % 10) ;
}
return 0 ;
}
3、使用多线程
解释
如果程序中有可以同步进行的代码,那么使用多线程就可以同步计算他们,运行效率大大提升。
测试
注:本代码在windows10系统devc++上运行有效
#include <stdio.h>
#include <math.h>
#include <windows.h>
#include <time.h>
typedef struct Input{ long long from,to ; } Input ;
bool is_prime(long long num) ;
DWORD WINAPI ThreadProc(LPVOID) ;
long long func1() ;
long long func2() ;
int main(){
long long s = 0 ;
clock_t start = clock() ;
s = func1() ;
printf("不变化:%lld : %llums\n",s,clock() - start) ;
start = clock() ;
s = func2() ;
printf("开线程:%lld : %llums\n",s,clock() - start) ;
return 0 ;
}
long long func1(){
long long s = 0 ;
for(long long i = 1;i < 1000000;++ i)
if(is_prime(i))++ s ;
return s ;
}
long long func2(){
Input a[5] = {{1,200000},
{200001,400000},
{400001,600000},
{600001,800000},
{800001,1000000}} ;
HANDLE h[5] ;
unsigned long ID[5] ;
for(int i = 0;i < 5;++ i)
h[i] = CreateThread(NULL,1,ThreadProc,a + i,1,ID + i) ;
WaitForMultipleObjects(5,h,TRUE,INFINITE) ;
long long ts = 0,s = 0 ;
for(int i = 0;i < 5;++ i){
GetExitCodeThread(h[i],(LPDWORD)(&ts)) ;
s += ts ;
}
return s ;
}
DWORD WINAPI ThreadProc(LPVOID lpParameter) {
Input* a = (Input*)lpParameter ;
++ (a -> to) ;
long long s = 0 ;
while((a -> from) < (a -> to)){
if(is_prime(a -> from)) ++ s ;
++ (a -> from) ;
}
ExitThread(s) ;
}
bool is_prime(long long num){
if(num < 2)return false ;
if(num < 4)return true ;
if((num % 6) != 1 && (num % 6) != 5)return false ;
const int end = sqrt(num) + 1 ;
for(int i = 2;i < end;++ i)
if(!(num % i))return false ;
return true ;
}
4、减少除法运算
解释
无论是正数负数、整数小数,除法运算都需要大量的运算量,所以尽量优化为加、减、乘的运算可以节省时间。例如:a/2>b慢于a>b*2慢于a>b+b(当然,容易溢出的情况除外)
测试
#include <stdio.h>
int main(){
printf("请输入双方战力:") ;
int a,b ;
scanf("%d%d",&a,&b) ;
//if((a / 2) > b)
//else if((b * 2) < a)
else if((b + b) < a)
printf("第一方战力碾压!") ;
//else if((b / 2) > a)
//else if((a * 2) < b)
else if((a + a) < b)
printf("第二方战力碾压!") ;
else
printf("双方斗得难舍难分!") ;
return 0 ;
}
5、减少拷贝(值传递),使用指针、引用、移动资源传递
解释
每一次拷贝都会产生一个新的临时变量,不但浪费内存,而且浪费时间。(1)如果一块内存、一个变量要长期使用,那么就可以使用指针、引用减少拷贝,加快运行速度。(2)当一块内存即将释放时,如果还需要使用,则把它的资源给其他的变量进行移动赋值、构造,避免了内存的多次申请、释放,减少内存碎片,也加快运行速度(多用于占用内存较大的对象,对应移动拷贝、移动赋值函数,使用标准库函数move等可以实现)
两种情况,打个比方就是这样:
(1)图书馆里有很多的书,其中有一本就是小明喜欢看的(需要获取变量的值)。此时,小明想要看这一本书,他可以那一个空本子(新创建的临时变量),靠着自己的天才画工,照着这本书一模一样的临摹一本出来(拷贝传递,值传递,复制保存的值),可这太浪费时间了。于是,小明就可以用小纸条(指针,引用)记下图书在图书馆中的位置(变量的地址),每次想看这本书时,只要根据记录下的位置找到这本书就能看了(寻址访问),就可以大大节省时间(由于无需拷贝复制)。(假设小明走路去图书馆所花的时间十分短,可以忽略不计)
(2)小壮有一本书,小明很喜欢(需要获取变量的值),并且小壮不喜欢,要把它扔掉(当前变量即将被释放)作为天才画家的小明,当然可以照着那本书临摹一本出来(拷贝复制),可实在不方便。那么,小壮就决定把书送给小明(资源传递),这样小明就不用大费周章了,也节约了纸张(节省时间、内存)
测试
#include <stdio.h>
#include <stdlib.h>
template<typename _Tp>
struct remove_reference
{ typedef _Tp type ; } ;
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp type ; } ;
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp type ; } ;
template <typename T>
typename remove_reference<T> :: type&& move(T&& t) noexcept{
return static_cast<typename remove_reference<T> :: type&&>(t) ;
}
int a,b,c ;
class abc{
public :
abc(){
data = new int[100000] ;
for(int i = 0;i < 100000;++ i)
data[i] = rand() % 100 ;
++ a ;
}
abc(abc& t){
data = new int[100000] ;
for(int i = 0;i < 100000;++ i)
data[i] = t.data[i] ;
++ b ;
}
abc(abc&& t){
data = t.data ;
t.data = 0 ;
}
~abc() {
++ c ;
if(data)delete data ;
}
int* data ;
} ;
abc get(){
static abc t ;
return t ;
}
abc* get2(){
static abc t ;
return &t ;
}
abc& get3(){
static abc t ;
return t ;
}
abc get4(){
abc t ;
return move(t) ;
}
int main(){
a = b = c = 0 ;
abc t1 = get() ;
printf("普通构造%d次,拷贝构造%d次,析构函数%d次\n",a,b,c) ;
a = b = c = 0 ;
abc* t2 = get2() ;
printf("普通构造%d次,拷贝构造%d次,析构函数%d次\n",a,b,c) ;
a = b = c = 0 ;
const abc& t3 = get3() ;
printf("普通构造%d次,拷贝构造%d次,析构函数%d次\n",a,b,c) ;
a = b = c = 0 ;
abc t4 = get4() ;
printf("普通构造%d次,拷贝构造%d次,析构函数%d次\n",a,b,c) ;
return 0 ;
}
可以看到只有值传递的方式会多使用一次拷贝构造函数(第四种移动构造方式的一次析构是因为其他的几个函数中的t都是静态常量,会保持不释放,属于全局变量,而第四种方法适用于资源转移,所以原先的对象会进行一次析构,但不会释放data内存块)