189 8069 5689

提高C++性能的编程技巧-创新互联

前言

这里记录一些在使用C++进行编程时,可以提高软件性能的小技巧。

在五原等地区,都构建了全面的区域性战略布局,加强发展的系统性、市场前瞻性、产品创新能力,以专注、极致的服务理念,为客户提供网站设计、成都做网站 网站设计制作定制网站,公司网站建设,企业网站建设,成都品牌网站建设,全网整合营销推广,外贸网站制作,五原网站建设费用合理。

文章目录
  • 前言
  • 技巧一
  • 技巧二
  • 技巧三
  • 技巧四
  • 技巧五
  • 技巧六
  • 技巧七
  • 技巧八
  • 技巧九
  • 技巧十
  • 技巧十一
  • 技巧十二
  • 技巧十三
  • 技巧十四
  • 技巧十五
  • 技术十六
  • 总结
  • 参考资料


程序员对C++的性能有几个公认的基本原则:
①I/O的开销是高昂的;
②函数调用的开销是要考虑的一个因素,因此我们应该将短小的、频繁调用的函数内联;
③复制对象的开销是高昂的,最好选择传递引用,而不是传递值。
④在与生产目标具有同样特性的机器上运行性能测试。在其他机器上的测试结果可能不能准确地反映出优化对产品的影响,因此进行这些优化可能会浪费时间,甚至导致性能变得更糟糕。
⑤不做任何假设。对需要优化的地方的直觉往往是错误的。永远要比对优化前后的测试数据。
⑥首先要正确地设计,且仅在此基础上采取优化措施。除非万不得已,否则不要为了引入某些优化而牺牲系统的可维护性和可读性。首先要正确地设计。

性能差的代码示例:

class Trace {public:
    Trace(const string &name);
    ~Trace();
    void debug(const string &msg);
    static bool traceIsActive;
private:
    string theFunctionName;
};
inline Trace::Trace(const string &name) : theFunctionName(name) {if (traceIsActive) {cout<< “Enter function ”<< name<< endl;
    }
}
inline void Trace::debug(const string &msg) {if (traceIsActive) {cout<< msg<< endl;
    }
}
inline Trace::~Trace() {if (traceIsActive) {cout<< “Exit function ”<< theFunctionName<< endl;
    }
}
int myFunction(int x) {string name = “myFunction”;
    Trace t(name);
    ...
    string moreInfo = “more interesting info”;
    t.debug(moreInfo);
    ...
}; // 跟踪析构函数 将退出事件记录到一个输出流中

性能优化改进后的代码示例:

class Trace {public:
    Trace(const char *name); // char *要快于string
    ~Trace();
    void debug(const char *msg);
    static bool traceIsActive;
private:
    string *theFunctionName;
};
inline Trace::Trace(const char *name) : theFunctionName(0) {if (traceIsActive) {cout<< “Enter function ”<< name<< endl;
        theFunctionName = new string(name);
    }
}
inline void Trace::debug(const char *msg) {if (traceIsActive) {cout<< msg<< endl;
    }
}
inline Trace::~Trace() {if (traceIsActive) {cout<< “Exit function ”<< theFunctionName<< endl;
        delete theFunctionName;
    }
}

对象定义会触发隐形地执行构造函数和析构函数。对象的构造和销毁并不总是意味产生开销,如果构造函数和析构函数所执行的计算是必须的,那么就要考虑使用高效的代码(内联会减少函数调用和返回的开销)。

技巧一

如果我们不需要用string对象的强大功能去做高深的事情,则完全可以将它替换为char指针,一个char指针的构造比一个string对象廉价的多。

技巧二

对于类的复合对象,为了对子对象的创建和销毁进行更好的控制,可以用指针来代替它。但如果使用模式是跟踪永远打开,则将子对象嵌入到主对象中,效率将会更高,因为它占用的是栈内存而不是堆内存。堆内存的分配与释放代价是相当高昂的,基于栈的内存在编译时分配,而在函数调用返回时的堆栈清除阶段被释放。

技巧三

简化封装。对象的创建(或销毁)触发对父对象和成员对象的递归创建(或销毁),它们使得创建和销毁的开销更高昂。

技巧四

编译器必须初始化被包含的成员对象之后再执行构造函数体。你必须在初始化阶段完成成员对象的创建,这可以降低随后在构造函数部分调用赋值操作符的开销。在某些情况下,这样也可以避免临时对象的产生。

技巧五

未利用RVO进行性能优化的示例:

Complex operator+(const Complex &a, const Complex &b) {Complex retVal;
    retVal.real = a.real + b.real;
    retVal.imag = a.imag + b.imag;
    return retVal;
}

利用RVO进行性能优化的示例:

Complex operator+(const Complex &a, const Complex &b) {double r = a.real + b.real;
    double i = a.imag + b.imag;
    return Complex(r, i);
}

即避免不必要的临时变量。

技巧六

临时对象会以构造函数和析构函数的形式降低一半的性能。
例1,不好的写法:

Rational r2 = Rational(100);
Rational r3 = 100;

好的写法:

Rational r1(100);

例2,不好的写法:

string s1 = “Hello”;
string s2 = “World”;
string s3;
s3 = s1 + s2;

好的写法:

string s1 = “Hello”;
string s2 = “World”;
string s3 = s1 + s2;

例3,不好的写法:

s5 = s1 + s2 + s3 + s4; // 产生3个临时对象

好的写法:

s5 = s1;
s5 += s2;
s5 += s3;
s5 += s4;
技巧七

如果主要分配限于单线程的内存块,那么内存管理器也会有类似的性能提高。由于省去了全局函数new()和delete()必须处理的并发问题,单线程内存管理器的性能会有所提高。

// 用来把不同大小的内存块连接起来形成块序列的类
class MemoryChunk {public:
    MemoryChunk(MemoryChunk *nextChunk, size_t chunkSize);
    ~MemoryChunk();
    inline void *alloc(size_t size);
    inline void free(void *someElement);
    // 指向列表下一内存块的指针
    MemoryChunk *nextMemChunk() {return next;
    }
    // 当前内存块剩余空间大小
    size_t spaceAvailable() {return chunkSize - bytesAlreadyAllocated;
    }
    // 这是一个内存块的默认大小
    enum {DEFAULT_CHUNK_SIZE = 4096 };
private:
    MemoryChunk *next;
    void *mem;
    // 一个内存块的默认大小
    size_t chunkSize;
    // 当前内存块中已分配的字节数
    size_t bytesAlreadyAllocated;
};
MemoryChunk::MemoryChunk(MemoryChunk *nextChunk, size_t reqSize) {chunkSize = (reqSize >DEFAULT_CHUNK_SIZE) ? reqSize : DEFAULT_CHUNK_SIZE;
    next = nextChunk;
    bytesAlreadyAllocated = 0;
    mem = new char[chunkSize];
}
MemoryChunk::~MemoryChunk() {delete [] mem;
}
void *MemoryChunk::alloc(size_t requestSize) {void *addr = static_cast(static_castmem + bytesAlreadyAllocated);
    bytesAlreadyAllocated += requestSize;
    return addr;
}
inline void MemoryChunk::free(void *someElement) {}
// 用来实现可变大小内存管理的类
class ByteMemoryPool {public:
    ByteMemoryPool(size_t initSize=MemoryChunk::DEFAULT_CHUNK_SIZE);
    ~ByteMemoryPool();
    // 从私有内存池分配内存
    inline void *alloc(size_t size);
    // 释放先前从内存池中分配的内存
    inline void free(void *someElement);
private:
    // 内存块列表 它是我们的私有存储空间
    MemoryChunk *listOfMemoryChunks;
    // 向我们的私有存储空间添加一个内存块
    void expandStorage(size_t reqSize); 
};
ByteMemoryPool::ByteMemoryPool(size_t initSize) {expandStorage(initSize);
}
ByteMemoryPool::~ByteMemoryPool() {MemoryChunk *memChunk = listOfMemoryChunks;
    while (memChunk) {listOfMemoryChunks = memChunk->nextMemChunk();
        delete memChunk;
        memChunk = listOfMemoryChunks;
    }
}
void *ByteMemoryPool::alloc(size_t requestSize) {size_t space = listOfMemoryChunks->spaceAvailable();
    if (space< requestSize) {expandStorage(requestSize);
    }
    return listOfMemoryChunks->alloc(requestSize);
}
inline void ByteMemoryPool::free(void *someElement) {listOfMemoryChunks->free(someElement);
}
void ByteMemoryPool::expandStorage(size_t reqSize) {listOfMemoryChunks = new MemoryChunk(listOfMemoryChunks, reqSize);
}
// 测试内存管理器效果的类
class Rational {public:
    Rational(int a=0, int b=1) : n(a), d(b) {}
    void *operator new(size_t size) {return memPool->alloc(size);
    }
    void operator delete(void *doomed, size_t size) {memPool->free(doomed);
    }
    static void newMemPool() {memPool = new ByteMemoryPool;
    }
    static void deleteMemPool() {delete memPool;
    }
private:
    int n; // 分子 
    int d; // 分母
    static ByteMemoryPool *memPool;
};
// 测试实际代码
MemoryPool*Rational::memPool = 0;
int main() {...
    Rational *array[1000];
    Rational::newMemPool();
    // 此处开始计时
    for (int j=0; j<500; j++) {for (int i=0; i<1000; i++) {array[i] = new Rational(i);
        }
        for (int i=0; i<1000; i++) {delete array[i];
        }
    }
    // 此处停止计时
    Rational::deleteMemPool();
    ...
}

因为单线程内存管理器要比多线程内存管理器快得多,所以如果要分配的大多数内存块限于单线程中使用,那么可以显著提升性能。
书中给出的多线程内存管理器,由于涉及模板和系统平台特有功能函数的使用,没有借鉴价值。

技巧八

使用内联有时会适得其反,尤其是滥用的情况下。内联可能会使代码量变大,而代码量增多后会较原先出现更多的缓存失败和页面错误。
所以傻瓜式的方法是不要人工指定函数内联,完全交给编译器进行优化。

技巧九

当向vector插入大量自定义类型对象时,对象的拷贝构造函数和析构函数开销相当昂贵,并且向量的容量很有可能继续增长,这时可通过保存指针而不是对象来身份避免这种昂贵的代价。这是因为对象指针没有相关的构造函数和析构函数,复制指针的代价本质上和复制整数是相同的。例如:

vectorv;
v->push_back(new BigInt{10});
技巧十

在很多情况下,我们可以对在特定情况下要尽可能足够大的向量容量进行估计。在你有把握做出恰当估计的情况下,我们可以预留好必要的容量,例如:

vector*v = new vector;
v->reserve(size);
vectorInsert(v, dataBigInt, size);
技巧十一

当访问数据时,最先搜索的是数据缓存。若数据不在缓存中,硬件产生缓存失败信号,该信号会从RAM或硬盘加载数据至缓存中。缓存以缓存行为单位,通常加载比我们所寻找的特定数据项更大的一块数据。这样的话,在4字节整数上的缓存失败可能导致加载128字节的缓存行到缓存中。由于相关数据项的位置在内存中很可能相邻,因此这对我们很有用。
一个没有充分利用缓存行的示例:

class X {public:
    X() : a(1), c(2) {}
    ...
private:
    int a;
    char b[4096]; // 缓冲区
    int c;
};

利用缓存行的示例:

class X {...
private:
    int a;
    int c;
    char b[4096];
};

现在a和c更有可能位于相同的缓存行,因为a在c之前被访问,所以当我们需要访问c的时候,基本可以保证c在数据缓存中。

技巧十二

动态分配和释放堆内存的代价比较昂贵。从性能角度来讲,使用不需要显式管理的内存所产生的代价要低得多。被定义成局部变量的对象存放于堆栈上。该对象所占用的堆栈空间是为相应函数预留的堆栈空间的一部分,该对象被定义在这个函数范围内。
一个不好的示例:

void f() {X *xPtr = new X;
    ...
    delete xPtr;
}

一个更好的示例,定义类型X的局部对象:

void f() {X x;
    ...
} // 不需要释放x的内存

在后一种实现中,对象x驻留在堆栈上,因而不需要事先为其分配内存,也不需要在函数退出时释放内存。当f()返回时,堆栈内存会自动释放,这样就避免了调用new()和delete()的巨大代价。
成员数据中也存在类似的问题,但这次不是堆和栈内存之间的问题,而是选择将指针还是整个对象嵌入到包含对象中的问题。
一个不好的示例:

class Z {public:
    Z() : xPtr(new X) {... }
    ~Z() {delete xPtr; }
private:
    X *xPtr;
    ...
};

在构造函数中调用new()和在析构函数中调用delete()所产生的开销明显地增加了对象Z的代价。
一个更好的示例,在Z中嵌入对象X来消除内存管理的代价:

class Z {public:
    Z() {... }
    ~Z() {... }
private:
    X x;
    ...
};
技巧十三

通常情况下,编译器默认根本不会进行任何优化,这意味着这些重要的性能优化将不会生效,即使在代码中使用了关键字register和inline也无济于事。编译器会自动忽略这些关键字,而且它经常这样做。为了更好地利用这些优化手段,必须通过向命令行添加开关-O或者在GUI界面上选择性能优化选项。

技巧十四

缓存的原子单元以行为单位,一般来说一个缓存行可以存储大量字节,典型的缓存行有128字节。当从主内存加载4字节的整数时,并不是仅加载这4个字节,而是把包含它的整个行立即加载到缓存。当另外的缓存(在不同的处理器上运行)使这个整数无效时,整个缓存行都是无效的。因此,变量在物理内存中的布局十分重要。例如,HTStats类如果去掉smpDmz字符数组,会使两个锁相互靠近:

class HTStats {int httpReqs;
    int httpBytes; 
    pthread_mutex_t lockHttp;
    char smpDmz[CACHE_LINE_SIZE];
    int sslReqs;
    int sslBytes;
    pthread_mutex_t lockSsl;
    ...
};

两个锁lockHttp和lockSsl只相距8字节,它们很可能驻留在同一缓存行上。在P1和P2上运行的线程将不断使对方驻留的两个锁的缓存行失效。缓存一致性风暴将会严重降低性能和可扩展性,同时缓存命中率会降低到90%以下。而插入smpDmz字符数组可保证两个锁不会共享缓存行。理想情况下,锁应该放置在最靠近它所保护的共享数据附近。

技巧十五

如果所有线程都要修改一个共享资源,读/写锁将不会有任何的帮助。实际上,这种类型的锁会降低性能,因为它们的实现更为复杂,所以性能就低于普通锁。但如果你的共享数据在绝大多数时间里在执行读操作,而读/写锁将消除读者线程间的竞争,可以提高扩展性。

技术十六

一个实现良好的线程池示例:

// Work.hpp
#includeclass Work {public:
    static const int DefaultId{0};
    Work(int id=DefaultId) : id_{id}, executeFunction_{[]{}} {}
    Work(std::functionexecuteFunction, int id=DefaultId) : id_{id}, executeFunction_{executeFunction} {}
    void execute() {executeFunction_();
    }
    int id() const {return id_;
    }
private:
    int id_;
    std::functionexecuteFunction_;
};
// ThreadPool.hpp
#include#include#include#include#include 
#include#include#include “Work.h”
class ThreadPool {public:
    virtual ~ThreadPool() {stop();
    }
    void start(unsigned int numberOfThreads=1) {for (unsigned int i{0u}; ithreads_.push_back(std::thread(&ThreadPool::worker, this));
        }
    }
    void stop() {done_ = true;
        for (auto &thread: thread_) {thread.join();
        }
    }
    bool hasWork() {std::lock_guardblock(mutex_);
        return !workQueue_.empty();
    }
    virtual void add(Work work) {std::lock_guardblock(mutex_);
        workQueue_.push_front(work);
    }
    Work pullWork() {std::lock_guardblock(mutex_);
        if (workQueue_empty()) {return Work{};
        }
        auto work = workQueue_.back();
        workQueue_.pop_back();
        return work;
    }
private:
    void worker() {while (!done_) {while (!done_ && !hasWork()) {;
            }
            if (done_) {break;
            }
            pullWork().execute();
        }
    }
    std::atomicdone_{false};
    std::dequeworkQueue_;
    std::mutex mutex_;
    std::shared_ptrworkThread_;
    std::vectorthreads_;
};
// ThreadPoolTest.cpp
#include#include#include#include#include#include "ThreadPool.h"
using namespace std;
using std::chrono::milliseconds;
TEST_GROUP(AThreadPool) {mutex m;
   ThreadPool pool;
};
TEST(AThreadPool, HasNoWorkOnCreation) {CHECK_FALSE(pool.hasWork());
}
TEST(AThreadPool, HasWorkAfterAdd) {pool.add(Work{});
   CHECK_TRUE(pool.hasWork());
}
TEST(AThreadPool, AnswersWorkAddedOnPull) {pool.add(Work{1});
   auto work = pool.pullWork();
   LONGS_EQUAL(1, work.id());
}
TEST(AThreadPool, PullsElementsInFIFOOrder) {pool.add(Work{1});
   pool.add(Work{2});
   auto work = pool.pullWork();
   LONGS_EQUAL(1, work.id());
}
TEST(AThreadPool, HasNoWorkAfterLastElementRemoved) {pool.add(Work{});
   pool.pullWork();
   CHECK_FALSE(pool.hasWork());
}
TEST(AThreadPool, HasWorkAfterWorkRemovedButWorkRemains) {pool.add(Work{});
   pool.add(Work{});
   pool.pullWork();
   CHECK_TRUE(pool.hasWork());
}
class ThreadPoolThreadTests: public Utest {public:
   ThreadPool pool;
   mutex m;
   condition_variable wasExecuted;
   unsigned int count{0};
   vector>threads;
   void teardown() override {  for (auto& t: threads) t->join();
   }   
   void incrementCountAndNotify() {  std::unique_locklock(m); 
      ++count;
      wasExecuted.notify_all(); 
   }
   void waitForCountAndFailOnTimeout(
         unsigned int expectedCount, 
         const milliseconds& time=milliseconds(500)) {  unique_locklock(m);
      CHECK_TRUE(wasExecuted.wait_for(lock, time, 
            [&] {return expectedCount == count; }));
   }
};
TEST_GROUP_BASE(AThreadPool_AddRequest, ThreadPoolThreadTests) {void setup() override {  pool.start();
   }
};
TEST(AThreadPool_AddRequest, PullsWorkInAThread) {Work work{[&] {incrementCountAndNotify(); }};
   unsigned int NumberOfWorkItems{1};
   pool.add(work);
   waitForCountAndFailOnTimeout(NumberOfWorkItems);
}
TEST(AThreadPool_AddRequest, ExecutesAllWork) {Work work{[&] {incrementCountAndNotify(); }};
   unsigned int NumberOfWorkItems{3};
   for (unsigned int i{0}; i< NumberOfWorkItems; i++)
      pool.add(work);
   waitForCountAndFailOnTimeout(NumberOfWorkItems);
}
TEST(AThreadPool_AddRequest, HoldsUpUnderClientStress) {Work work{[&] {incrementCountAndNotify(); }};
   unsigned int NumberOfWorkItems{100};
   unsigned int NumberOfThreads{100};
   for (unsigned int i{0}; i< NumberOfThreads; i++)
      threads.push_back(
          make_shared([&] { for (unsigned int j{0}; j< NumberOfWorkItems; j++)
               pool.add(work); 
          }));
   waitForCountAndFailOnTimeout(
         NumberOfThreads * NumberOfWorkItems);
}
TEST_GROUP_BASE(AThreadPoolWithMultipleThreads, ThreadPoolThreadTests) {setthreads;
   void addThreadIfUnique(const thread::id& id) {  std::unique_locklock(m); 
      threads.insert(id);
   }
   size_t numberOfThreadsProcessed() {  return threads.size();
   }
};
TEST(AThreadPoolWithMultipleThreads, DispatchesWorkToMultipleThreads) {unsigned int numberOfThreads{2};
   pool.start(numberOfThreads);
   Work work{[&] {  addThreadIfUnique(this_thread::get_id());
      incrementCountAndNotify();
   }};
   unsigned int NumberOfWorkItems{500};
   for (unsigned int i{0}; i< NumberOfWorkItems; i++)
      pool.add(work);
   waitForCountAndFailOnTimeout(NumberOfWorkItems);
   LONGS_EQUAL(numberOfThreads, numberOfThreadsProcessed());
}
总结

以上就是使用C++在编写代码时,提高软件性能的一些小技巧。

参考资料

《提高C++性能的编程技术》,电子工业出版社
《C++程序设计实践与技巧 测试驱动开发》,人民邮电出版社

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


当前名称:提高C++性能的编程技巧-创新互联
标题网址:http://cdxtjz.cn/article/jcopd.html

其他资讯