1 提前求值
1 概念
上一篇介绍了“延缓求值”——lazy evalute策略,其实质是:只有在真正需要数据的时候,才对计算进行求值。同时,常用的一种的策略是“马上求值”——eager evaluate,即只要出现计算表达式,就进行求值。
上面两种方案都没有考虑到,一次性大规模计算会让用户长时间等待。基于此,本篇提出“超急求值”——over eager evaluate。它实质是一种分摊(也可以说是分治)策略。
如下面代码,若otherFunc()会先于getMin()和getMax()调用,则m_max和m_min的计算可以放到otherFunc()中,而不必在每次需要min和max两个值时,都对数据集进行遍历,再求出min和max。
class ScoreList {
public:
void otherFunc();
double getMin();
double getMax();
private:
double m_max, m_min;
}
这种方法通过将大规模计算分摊到其他操作中,并将计算结果永久性保存下来,降低了单次用户响应时间——注意,由于执行的语句数量不变,因此程序总运行响应时间不会有很大变化。
2 应用场景
下面提出如下几种应用场景。
1,cache(缓存)策略: 如果内存空间足够大,可以在进行其他操作的时候,顺带将磁盘中的数据库数据读取到内存中,每次访问某条数据,直接读取内存中的变量即可。
2,prefetch(预取出)策略:cache策略只取出并暂存需要的数据。由于2/8原则,可以当前需要数据临近区域的数据取出作进一步缓存,存储在内存中,从而提高数据访问速度。
3,std::vector的空间分配:每次vector的空间耗尽时,若继续向vector中存储数据,系统会做如下操作:
分配新空间 -> 复制原有数据到新空间 -> 将新数据插入到新空间。
新分配的空间在内存扩张时,将分配2倍原有空间,以防止后续插入操作,再次引起原有数据的复制操作。
3 总结
至此,关于求值策略,我们提出三种方法:马上求值,延缓求值,超前求值。三种方法无非就是“时间与空间的博弈”——用延长时间换空间减小,或者用空间减小换时间缩短。
2 临时对象的来源
临时对象并非下面代码所示,在函数中声明一个变量temp,temp只是函数中的局部对象。
void func() {
int temp = 0;
// do something
}
临时变量有两个来源:
1,隐式类型转换。
2,函数返回的对象。
1 隐式类型转换
如下面代码所示,隐式类型转换有多种形式。
其中,情形2的转换,只会发生在"按值传参"和“const 引用传参”的情况下, 这两种允许兼容的不同类型对象进行相互转换。“引用传参”和“指针传参”会严格按照类型进行匹配,不会出现类型转换。
// 情形1:1.23临时转换,得到一个临时int类型对象,然后赋值给a
int a = static_cast<int>(1.23);
// 情形2:实参与形参类型不一致,实参转换类型,生成string临时对象
int countChar(const std::string& str);
char *str = "hello world";
countChar(str); // 函数返回,临时对象被销毁
int countChar(std::string &str);
countChar(str); // 错误,char*和std::string&类型不兼容
2 返回值创建临时对象
这种情况指每次函数返回都会产生新的临时对象。如下面代码所示的集中情形。
// 情形1:返回变量greet引起临时变量创建,这个变量没有名称——匿名变量。
std::string createStr() {
std::string greet;
return greet;
}
// 情形2:调用operator + 操作符。a+c会产生新的临时变量,并赋值给a
// 优化方法:使用+=操作符,a += c
MyClass a = a + c
// 情形3:调用“前置自增”操作符,a++
int a = 0;
a++;
针对情形1,需要特别注意不能返回函数内局部变量的引用。如下面代码。
const std::string &createStr() {
return std::string();
}
// 或者
std::string &createStr() {
return std::string();
}
对于情形3, 需要注意前置自增实际上进行两个操作,先获得原始值,然后再进行自增运算。这样会产生原始值的临时变量。因此,若只是想做自增运算,要使用“后置自增”。
int a = 0;
++a; // 只是自增运算