• 提高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);
        ...
    }; // 跟踪析构函数 将退出事件记录到一个输出流中
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

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

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

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

    技巧一

    如果我们不需要用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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    利用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
    • 2
    • 3
    • 4
    • 5

    避免不必要的临时变量

    技巧六

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

    Rational r2 = Rational(100);
    Rational r3 = 100;
    
    • 1
    • 2

    好的写法:

    Rational r1(100);
    
    • 1

    例2,不好的写法:

    string s1 = “Hello”;
    string s2 = “World”;
    string s3;
    s3 = s1 + s2;
    
    • 1
    • 2
    • 3
    • 4

    好的写法:

    string s1 = “Hello”;
    string s2 = “World”;
    string s3 = s1 + s2;
    
    • 1
    • 2
    • 3

    例3,不好的写法:

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

    好的写法:

    s5 = s1;
    s5 += s2;
    s5 += s3;
    s5 += s4;
    
    • 1
    • 2
    • 3
    • 4

    技巧七

    如果主要分配限于单线程的内存块,那么内存管理器也会有类似的性能提高。由于省去了全局函数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<void*>(static_cast<size_t>mem + 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> *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();
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119

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

    技巧八

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

    技巧九

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

    vector<BigInt*> v;
    v->push_back(new BigInt{10});
    
    • 1
    • 2

    技巧十

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

    vector<BigInt> *v = new vector<BigInt>;
    v->reserve(size);
    vectorInsert(v, dataBigInt, size);
    
    • 1
    • 2
    • 3

    技巧十一

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

    class X {
    public:
        X() : a(1), c(2) {}
        ...
    private:
        int a;
        char b[4096]; // 缓冲区
        int c;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    利用缓存行的示例:

    class X {
        ...
    private:
        int a;
        int c;
        char b[4096];
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    技巧十二

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

    void f() {
        X *xPtr = new X;
        ...
        delete xPtr;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    void f() {
        X x;
        ...
    } // 不需要释放x的内存
    
    • 1
    • 2
    • 3
    • 4

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

    class Z {
    public:
        Z() : xPtr(new X) { ... }
        ~Z() { delete xPtr; }
    private:
        X *xPtr;
        ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    class Z {
    public:
        Z() { ... }
        ~Z() { ... }
    private:
        X x;
        ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    技巧十三

    通常情况下,编译器默认根本不会进行任何优化,这意味着这些重要的性能优化将不会生效,即使在代码中使用了关键字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;
        ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    技巧十五

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

    技术十六

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

    // Work.hpp
    #include 
    class Work {
    public:
        static const int DefaultId{0};
        Work(int id=DefaultId) : id_{id}, executeFunction_{[]{}} {}
        Work(std::function<void()> executeFunction, int id=DefaultId) : id_{id}, executeFunction_{executeFunction} {}
        void execute() {
            executeFunction_();
        }
        int id() const {
            return id_;
        }
    private:
        int id_;
        std::function<void()> executeFunction_;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    // 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}; i<numberOfThreads; i++) {
                threads_.push_back(std::thread(&ThreadPool::worker, this));
            }
        }
        void stop() {
            done_ = true;
            for (auto &thread: thread_) {
                thread.join();
            }
        }
        bool hasWork() {
            std::lock_guard<std::mutex> block(mutex_);
            return !workQueue_.empty();
        }
        virtual void add(Work work) {
            std::lock_guard<std::mutex> block(mutex_);
            workQueue_.push_front(work);
        }
        Work pullWork() {
            std::lock_guard<std::mutex> block(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::atomic<bool> done_{false};
        std::deque<Work> workQueue_;
        std::mutex mutex_;
        std::shared_ptr<std::thread> workThread_;
        std::vector<std::thread> threads_;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    // 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<shared_ptr<thread>> threads;
       void teardown() override {
          for (auto& t: threads) t->join();
       }   
       void incrementCountAndNotify() {
          std::unique_lock<std::mutex> lock(m); 
          ++count;
          wasExecuted.notify_all(); 
       }
       void waitForCountAndFailOnTimeout(
             unsigned int expectedCount, 
             const milliseconds& time=milliseconds(500)) {
          unique_lock<mutex> lock(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<thread>([&] { 
                 for (unsigned int j{0}; j < NumberOfWorkItems; j++)
                   pool.add(work); 
              }));
       waitForCountAndFailOnTimeout(
             NumberOfThreads * NumberOfWorkItems);
    }
    TEST_GROUP_BASE(AThreadPoolWithMultipleThreads, ThreadPoolThreadTests) {
       set<thread::id> threads;
       void addThreadIfUnique(const thread::id& id) {
          std::unique_lock<std::mutex> lock(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());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119

    总结

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

    参考资料

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

  • 相关阅读:
    `pip` 下载速度慢
    用于割草机器人,商用服务型机器人的陀螺仪
    ssm电影院管理系统的设计与实现毕业设计源码241505
    AndroidStudio activity-1.8.0.aar依赖报错
    ssm手机销售网站
    贪吃蛇游戏和俄罗斯方块
    O-RAN学习笔记之O-RAN、OpenRAN、C-RAN、vRAN、xRAN
    Mysql基本语法
    解析数仓lazyagg查询重写优化
    认识wireshark
  • 原文地址:https://blog.csdn.net/oruchimaru0420/article/details/128085983