More effective C++:效率(1)
Item M16:牢记 80-20 准则(80-20 rule)
80-20准则是大约20%的代码负责了80%的资源消耗、运行时间、内存使用、磁盘访问等关键性能指标,同时80%的维护工作也集中在大约20%的代码。
80-20准则的意义
不必过分担心大部分代码的性能,因为这些代码的效率对整体影响不大。当出现性能问题时,需要精确定位并优化那20%的关键代码。
很多开发者倾向于根据个人经验和直觉来猜测性能瓶颈所在,但这种方式往往不准确,甚至可能导致无效的优化努力。在I/O受限的应用中优化CPU性能,或是在CPU密集型任务中优化I/O性能,都不会带来明显的性能提升。使用性能分析工具(Profiler):应该依赖专业的性能分析工具来准确识别程序中的性能瓶颈。Profiler可以提供关于程序各部分运行时间、资源消耗等详细数据,帮助开发者集中精力优化真正重要的部分。最终目标是改善用户的体验,减少等待时间,而不是单纯地减少语句数量或函数调用次数。
Item M17:考虑使用 lazy evaluation
⭐关联知识点:copy-on-write
Lazy Evaluation 是一种延迟计算的策略,其核心思想是“推迟计算直到必要时”。只有当确实需要某个值时,程序才会进行相应的计算。这种方法可以减少不必要的计算,从而提高程序的效率。
1. 引用计数
在 C++ 中,std::string 类可以通过引用计数来实现懒惰计算。当一个字符串对象被复制时,不是立即创建一个新的字符串副本,而是让多个对象共享同一个底层数据。只有当其中一个对象需要修改时,才创建独立的副本。
class String {
public:
String(const char* str):data(new std::string(str)),refCount(new int(1)){}
String(const String& other) : data(other.data),refCount(other.refCount){(*refCount)++;}
~String(){decrementRefCount();}
void convertToUpperCase() {
if (*refCount > 1) {
data = new std::string(*data);
refCount = new int(1);
}
for (char& c : *data) {
c = toupper(c);
}
}
private:
std::string* data;
int* refCount;
void decrementRefCount() {
if (--(*refCount) == 0) {
delete data;
delete refCount;
}
}
};
int main() {
String s1 = "Hello";
String s2 = s1; // 不立即创建副本
s2.convertToUpperCase(); // 创建副本并修改
return 0;
}
2. 区别对待读取和写入
在 operator[] 的实现中,可以区分读操作和写操作。读操作可以直接返回共享数据,而写操作则需要创建独立的副本。
class String {
public:
char& operator[](size_t index) {
if (isShared()) {
makeUnique();
}
return data[index];
}
const char& operator[](size_t index) const {
return data[index];
}
private:
std::string data;
int* refCount;
bool isShared() const {
return refCount != nullptr && *refCount > 1;
}
void makeUnique() {
if (isShared()) {
data = std::string(data);
refCount = new int(1);
}
}
};
int main() {
String s = "Homer's Iliad";
std::cout << s[3]; // 读操作
s[3] = 'x'; // 写操作
return 0;
}
非常量版本:当 s1[0]='h'时,isShared()返回 true,因此调用 makeUnique()创建了一个独立的副本。修改 s1 的第一个字符后,s1 变为 "hello",而 s2 仍然是 "Hello"。
常量版本:当读取 s2 的第一个字符时,调用的是常量版本的 operator[],不会创建独立的副本,直接返回共享数据中的字符引用。
3. Lazy Fetching(懒惰提取)
在处理大型持久对象时,可以使用懒惰提取来延迟从数据库中读取数据,直到确实需要时才读取。
class LargeObject {
public:
LargeObject(ObjectID id):oid(id),field1Value(nullptr),field2Value(nullptr){}
const std::string& field1() const {
if (!field1Value) {
field1Value = new std::string(fetchFromDatabase(oid, "field1"));
}
return *field1Value;
}
int field2() const {
if (!field2Value) {
field2Value = new int(fetchFromDatabase(oid, "field2"));
}
return *field2Value;
}
private:
ObjectID oid;
mutable std::string* field1Value;
mutable int* field2Value;
std::string fetchFromDatabase(ObjectID id, const std::string& fieldName) const{
return "some value";
}
};
int main() {
LargeObject obj(123);
std::cout << obj.field2(); // 懒惰提取 field2
return 0;
}
在构造函数中,field1Value 和 field2Value 被初始化为 nullptr,表示这些字段尚未从数据库中读取。当调用 field1() 方法时,首先检查 field1Value 是否为 nullptr。如果 field1Value 为 nullptr,表示 field1 尚未从数据库中读取,此时调用 fetchFromDatabase 方法从数据库中读取 field1 的值,并将其存储在 field1Value 指向的新分配的 std::string 对象中,返回field1Value 指向的 std::string 对象的引用。
filed2同理。
4 懒惰计算
懒惰表达式计算是一种延迟计算的技术,尤其是在处理大型数据结构(如矩阵)时。通过懒惰计算,可以避免不必要的计算和内存分配,从而提高程序的性能。
class Matrix {
public:
Matrix(size_t rows, size_t cols) : rows(rows), cols(cols), data(rows * cols, 0) {}
// 懒惰计算的加法操作
Matrix operator+(const Matrix& other) const {
return MatrixExpression<T>(*this, other, '+');
}
// 懒惰计算的乘法操作
Matrix operator*(const Matrix& other) const {
return MatrixExpression<T>(*this, other, '*');
}
// 获取矩阵元素
T& operator()(size_t row, size_t col) {
return data[row * cols + col];
}
const T& operator()(size_t row, size_t col) const {
return data[row * cols + col];
}
// 显式计算矩阵
void evaluate() {
if (expression) {
expression->evaluate(*this);
expression = nullptr;
}
}
private:
size_t rows, cols;
std::vector<T> data;
std::unique_ptr<MatrixExpression<T>> expression;
// 表达式类
class MatrixExpression {
public:
MatrixExpression(const Matrix& lhs, const Matrix& rhs, char op)
: lhs(lhs), rhs(rhs), op(op) {}
void evaluate(Matrix& result) const {
if (op == '+') {
for (size_t i = 0; i < lhs.rows * lhs.cols; ++i) {
result.data[i] = lhs.data[i] + rhs.data[i];
}
} else if (op == '*') {
for (size_t i = 0; i < lhs.rows; ++i) {
for (size_t j = 0; j < rhs.cols; ++j) {
T sum = 0;
for (size_t k = 0; k < lhs.cols; ++k) {
sum += lhs(i, k) * rhs(k, j);
}
result(i, j) = sum;
private:
const Matrix& lhs;
const Matrix& rhs;
char op;
};
};
int main() {
Matrix<int>m1(1000, 1000);
Matrix<int>m2(1000, 1000);
// 懒惰计算m1+m2
Matrix<int>m3 = m1 + m2;
// 懒惰计算m4*m1
Matrix<int>m4(1000, 1000);
m3=m4*m1;
// 显式计算 m3 的第四行
m3.evaluate();
std::cout << m3(4, 0) << std::endl;
// 显式计算 m3 的所有值
m3.evaluate();
for (size_t i = 0; i < m3.rows; ++i) {
for (size_t j = 0; j < m3.cols; ++j) {
std::cout << m3(i, j) << " ";
}
std::cout << std::endl;
}
return 0;
}
懒惰表达式计算的优势:
(1)按需计算:只有在真正需要某个值时,才会进行计算。例如,如果只需要矩阵的一部分,可以避免计算整个矩阵。
(2)节省内存:通过延迟计算,可以减少不必要的内存分配。例如,如果只需要矩阵的某些行或列,可以避免分配整个矩阵的内存。
(3)提高性能:避免不必要的计算和内存分配,特别是在处理大型数据结构时,可以显著提高程序的性能。
懒惰计算 m1 + m2
Matrix<int> m3 = m1 + m2;
计算 m1 + m2,但不立即执行加法操作,而是创建一个 MatrixExpression 对象表示这个表达式。
按需计算 m3 的第四行第一个元素:
std::cout << m3(4, 0) << std::endl;
访问 m3 的第 4 行第 0 列的元素时,调用 evaluateIfNecessary(4, 0) 方法。evaluateIfNecessary 方法检测到存在懒惰表达式,调用 evaluatePartial 方法计算 m3(4, 0) 的值。
Item M18:分期摊还期望的计算
在条款 M18 中,作者讨论了 Over-Eager Evaluation(过度热情计算法) 的概念。 Eager Evaluation(热情计算法) 和 Lazy Evaluation(懒惰计算法) 不同,Over-Eager Evaluation 在需求出现之前就预先完成计算或准备工作,以提高后续操作的效率。
Eager Evaluation:当需要某个值时,立即计算并返回。
Lazy Evaluation:只有在真正需要某个值时,才进行计算。
Over-Eager Evaluation:在需求出现之前,预先完成计算或准备工作,以便在实际需要时能够快速响应。
1. DataCollection 类
template<class NumericalType>
class DataCollection {
public:
NumericalType min() const;
NumericalType max() const;
NumericalType avg() const;
...
};
假设 min, max 和 avg 函数分别返回集合的最小值、最大值和平均值。有三种实现方法:
Eager Evaluation:每次调用这些函数时,遍历整个集合并计算结果。
Lazy Evaluation:只有在确实需要这些值时,才计算并缓存结果。
Over-Eager Evaluation:随时跟踪集合的最小值、最大值和平均值,这样在调用这些函数时可以直接返回结果,无需重新计算。
Over-Eager Evaluation 实现
template<class NumericalType>
class DataCollection {
public:
void add(NumericalType value) {
data.push_back(value);
updateStatistics(value);
}
NumericalType min() const {
return minValue;
}
NumericalType max() const {
return maxValue;
}
NumericalType avg() const {
return averageValue;
}
private:
std::vector<NumericalType> data;
NumericalType minValue;
NumericalType maxValue;
NumericalType averageValue;
size_t count;
void updateStatistics(NumericalType value) {
if (count == 0) {
minValue = value;
maxValue = value;
averageValue = value;
} else {
if (value < minValue) minValue = value;
if (value > maxValue) maxValue = value;
averageValue = (averageValue * count + value) / (count + 1);
}
count++;
}
};
每次添加一个新元素时,不仅将元素添加到 data 向量中,还调用 updateStatistics 方法更新最小值、最大值和平均值。
定义静态缓存
typedef std::map<std::string, int> CubicleMap;
static CubicleMap cubes;
使用 std::map 作为缓存,存储员工姓名和隔间号的映射关系。
查找缓存:
CubicleMap::iterator it = cubes.find(employeeName);
尝试在缓存中查找员工的隔间号。
缓存命中:
if (it == cubes.end()) {
int cubicle = /* the result of looking up employeeName's cubicle number in the database */;
cubes[employeeName] = cubicle;
return cubicle;
} else {
return (*it).second;
}
如果缓存中没有找到员工的隔间号,则从数据库中查询,并将结果添加到缓存中。
如果缓存中已存在员工的隔间号,则直接返回缓存中的值。
预提取示例:DynArray 类
template<class T>
class DynArray {
public:
T& operator[](int index) {
if (index < 0) {
throw std::out_of_range("Negative index is not allowed");
}
if (index >= capacity) {
int newCapacity = std::max(capacity * 2, index + 1);
T* newData = new T[newCapacity];
for (int i = 0; i < capacity; ++i) {
newData[i] = data[i];
}
delete[] data;
data = newData;
capacity = newCapacity;
}
if (index >= size) {
size = index + 1;
}
return data[index];
}
private:
T* data = nullptr;
int capacity = 0;
int size = 0;
};
动态数组的索引操作:
T& operator[](int index) {
if (index < 0) {
throw std::out_of_range("Negative index is not allowed");
}
if (index >= capacity) {
int newCapacity = std::max(capacity * 2, index + 1);
T* newData = new T[newCapacity];
for (int i = 0; i < capacity; ++i) {
newData[i] = data[i];
}
delete[] data;
data = newData;
capacity = newCapacity;
}
if (index >= size) {
size = index + 1;
}
return data[index];
}
检查索引是否为负数,如果是则抛出异常。如果索引超出当前容量,预先分配更大的内存(通常是当前容量的两倍或直接满足索引需求)。如果索引超出当前大小,更新数组的大小。返回指定索引处的元素。
总结:(1)Over-Eager Evaluation:在需求出现之前预先完成计算或准备工作,以提高后续操作的效率。(2)Caching:缓存已经计算出来的结果,减少重复计算的开销。(3)Prefetching:预先分配更多资源,以减少未来扩展的开销。
Item M19:理解临时对象的来源
代码示例 1: swap 函数
template <class T>
void swap(T& object1, T& object2) {
T temp = object1; // 局部对象,而非临时对象
object1 = object2;
object2 = temp;
}
代码示例 2: countChar 函数
size_t countChar(const string& str, char ch) {
size_t count = 0;
for (char c : str) {
if (c == ch) ++count;
}
return count;
}
代码示例 3: uppercasify 函数
void uppercasify(string& str) {
for (char& c : str) {
c = toupper(c);
}
}
局部对象
(1) swap 函数中的 temp,它是一个有名字的局部对象,存在于函数的作用域内。当函数执行完毕后,该对象会被销毁。
(2)临时对象:C++ 中的临时对象是没有名字的。
隐式类型转换:当函数参数类型与实际传入的参数类型不匹配时,编译器可能会创建一个临时对象来进行类型转换。例如,在 countChar(buffer, c) 调用中,buffer 是一个 char[] 类型,但 countChar 的参数要求是 const string&,因此编译器会创建一个临时的 string 对象。函数返回对象:当一个函数返回一个对象时,返回的对象通常是一个临时对象。例如,operator+ 返回一个 Number 对象,这个对象是临时的,因为它没有名字,只是作为函数的返回值。
临时对象的开销
构造和析构:临时对象的创建和销毁都会带来一定的开销。特别是当对象较大或构造函数/析构函数复杂时,这种开销可能会影响程序性能。编译器可以通过一些优化技术 (返回值优化 RVO) 来减少临时对象的开销。
避免临时对象
尽量避免需要隐式类型转换的情况。例如,可以修改 countChar 函数,使其接受 const char* 类型的参数,而不是 const string&。
使用常量引用:对于不需要修改的参数,使用常量引用(const &)可以避免不必要的复制。
避免非常量引用:对于需要修改的参数,使用非常量引用(&)时,编译器不会创建临时对象,因为这会导致逻辑错误(临时对象被修改后立即销毁,实际参数不会改变)。
总结:通过合理的设计和优化,可以显著减少临时对象带来的性能开销。
Item M20:协助完成返回值优化
代码示例 1: Rational 类定义
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
private:
int num; // 分子
int den; // 分母
};
// 构造函数
Rational::Rational(int numerator, int denominator) {
if (denominator == 0) {
throw std::invalid_argument("Denominator cannot be zero");
}
num = numerator;
den = denominator;
}
// 获取分子
int Rational::numerator() const {
return num;
}
// 获取分母
int Rational::denominator() const {
return den;
}
代码示例 2: operator* 实现
// 返回值为 const 的原因详见条款 M6
const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
传值返回的开销:当一个函数返回一个对象时,通常需要调用构造函数和析构函数来创建和销毁临时对象。例如,operator* 函数返回一个 Rational 对象,这个过程涉及临时对象的创建和销毁。某些情况下,返回对象是不可避免的。例如,operator* 必须返回一个新的 Rational 对象来表示两个有理数的乘积。
错误的方法
(1)返回指针
虽然可以避免直接返回对象,但这种方法会导致资源管理问题,如内存泄漏。
const Rational * operator*(const Rational& lhs, const Rational& rhs) {
return new Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
使用这种方式时,调用者需要负责删除返回的指针,这容易出错。
(2)返回引用
返回引用也是错误的,因为返回的引用指向的局部对象在函数退出时会被销毁。
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
Rational result(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
return result; // 错误:返回的引用指向已销毁的局部对象
}
直接返回对象:最简单和最安全的方法是直接返回对象。编译器可以通过返回值优化(Return Value Optimization, RVO)来减少临时对象的开销。
返回值优化(RVO)
RVO 的原理:RVO 是一种编译器优化技术,允许编译器在函数返回时直接在目标变量的位置构造对象,从而避免临时对象的创建和销毁。
Rational a = 10;
Rational b(1, 2);
Rational c = a * b; // 在这里调用 operator*
编译器可以在 c 的内存位置直接构造 Rational 对象,而不是先创建一个临时对象再拷贝到 c。
通过 RVO可以显著减少临时对象的开销,提高程序的性能。现代编译器通常都支持 RVO,因此在编写返回对象的函数时,可以直接返回对象而不必担心性能问题。
最佳实践
(1)直接返回对象:在大多数情况下,直接返回对象是最简单和最安全的方法。编译器会自动应用 RVO 来优化性能。
(2)使用 inline 关键字:如果函数体较小,可以考虑将其声明为 inline,以进一步减少函数调用的开销。
原文地址:https://blog.csdn.net/m0_52043808/article/details/143726164
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!