自学内容网 自学内容网

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)!