c++ 代码技巧

数学运算性能

大多数数据运算不存在性能问题,但是相对来说,整型的除法运算还是比较昂贵的。
参考下面的例子:

uint32_t BM_S1(uint64_t v) {
  uint32_t result = 0;
  do {
    ++result;
    v /= 10;
  } while (v);
  return result;
}

uint32_t BM_S2(uint64_t v) {
  uint32_t result = 1;
  for (;;) {
    if (v < 10) return result;
    if (v < 100) return result + 1;
    if (v < 1000) return result + 2;
    if (v < 10000) return result + 3;
    v /= 10000;
    result += 4;
  }
  return result;
}

uint32_t BM_S3(uint64_t v) {
  uint32_t result = 1;
  for (;;) {
    if (v < 10) return 1;
    if (v < 100) return 2;
    if (v < 1000) return 3;
    if (v < 1000000000000) {  // 10^12
      if (v < 100000000) {    // 10^7
        if (v < 1000000) {    // 10^6
          if (v < 10000) {
            return 4;
          }
          return 5 + (v >= 100000);  // 10^5
        }
        return 7 + (v >= 10000000);  // 10^7
      }
      if (v < 10000000000) {           // 10^10
        return 9 + (v >= 1000000000);  // 10^9
      }
      return 11 + (v >= 100000000000);  // 10^11
    }
    v /= 1000000000000;
    result += 12;
  }
  return result;
}

image
核心原因是因为 BM_S2 用比较和加法减少了除法运算,BM_S3通过搜索的方法进一步减少了除法操作。
运算速度可以参考下面:

* comparisons  (1 clock cycle)
* (u)int add, subtract, bitops, shift (1 clock cycle)
* floating point add, sub (3~6 clock cycle) 
* indexed array access (cache effects)
* (u)int32 mul  (3~4 clock cycle)
* Floating point mul (4~8 clock cycle)
* Float Point division, remainder (14~45 clock cycle)
* (u)int division, remainder (40~80 clock cycle)

除法比乘法要慢很多,所以有些除法运算可以利用乘法来加快:

double y, a1, a2, b1, b2;
y = a1/b1 + a2/b2;  // slow
double y, a1, a2, b1, b2;
y = (a1*b2 + a2*b1) / (b1*b2); // faster

cache line

目前的计算机系统,cache line 是 64 字节,基于cache line 可以做一些优化,从而减少内存读取次数。但是有些场景,cache line 反而会拖慢,例如下面的结构体:

struct S {
  long long a;
  long long b;
};

如果两个线程,一个写a,一个写b,这样会频发的触发内存和cache的交互。通过添加占位,让a和b不在一个cache line上,性能会更好。

struct S2 {
  long long a;
  long long nonp[8];  // 占位,a、b 加载到不同的缓存行
  long long b;
};

选择删除vector中的某些元素

auto cond = [](int x) { return (x & 1 == 1); };
vec3.erase(std::remove_if(vec3.begin(), vec3.end(), cond), vec3.end());

时间复杂度是 O(n)

字符串拼接

void func1() {
  std::string a = "hello";
  std::string b = "world";
  std::string c = "";
  for (int i = 0; i < 10000; i++) {
    c = a + b;
  }
}

void func2() {
  std::string a = "hello";
  std::string b = "world";
  std::string c = "";
  for (int i = 0; i < 10000; i++) {
    c += a;
    c += b;
  }
}

void func3() {
  std::string a = "hello";
  std::string b = "world";
  std::string c = "";
  for (int i = 0; i < 10000; i++) {
    c.append(a);
    c.append(b);
  }
}

void func4() {
  std::stringstream ss;
  std::string a = "hello";
  std::string b = "world";
  for (int i = 0; i < 10000; i++) {
    ss << a;
    ss << b;
  }
}

image

使用 += 和 append 的拼接效率是最高的。

字符串优化

#include <benchmark/benchmark.h>
#include <string>

#define BENCHMARKFUNC(func)                                                             \
  static void BM_##func(benchmark::State& state) {                                      \
    for (auto _ : state) {                                                              \
      std::string s = "BBHFGDHGFDTREHBGFYRTAGFYGIWERWQAGFJNBVNRETWEAHGFFFUJHRWEAGHDFG"; \
      func(s);                                                                          \
    }                                                                                   \
  }                                                                                     \
  BENCHMARK(BM_##func);

std::string remove_1(std::string s) {
  std::string result;
  for (int i = 0; i < s.length(); ++i) {
    if (s[i] != 'A') {
      result = result + s[i];
    }
  }
  return result;
}
BENCHMARKFUNC(remove_1);

// std::string::length() 返回值是 size_t
std::string remove_2(std::string s) {
  std::string result;
  for (size_t i = 0; i < s.length(); ++i) {
    if (s[i] != 'A') {
      result = result + s[i];
    }
  }
  return result;
}
BENCHMARKFUNC(remove_2);

// 复合赋值操作替换拼接操作,避免临时字符串
std::string remove_3(std::string s) {
  std::string result;
  for (size_t i = 0; i < s.length(); ++i) {
    if (s[i] != 'A') {
      result += s[i];
    }
  }
  return result;
}
BENCHMARKFUNC(remove_3);

// 预留存储空间减少内存重新分配
std::string remove_4(std::string s) {
  std::string result;
  result.reserve(s.length());
  for (size_t i = 0; i < s.length(); ++i) {
    if (s[i] != 'A') {
      result += s[i];
    }
  }
  return result;
}
BENCHMARKFUNC(remove_4);

// 参数引用
std::string remove_5(const std::string& s) {
  std::string result;
  result.reserve(s.length());
  for (size_t i = 0; i < s.length(); ++i) {
    if (s[i] != 'A') {
      result += s[i];
    }
  }
  return result;
}
BENCHMARKFUNC(remove_5);

// 使用迭代器消除指针解引
// 初始化时调用end,缓存,避免后续重复计算
std::string remove_6(const std::string& s) {
  std::string result;
  result.reserve(s.length());
  for (auto it = s.begin(), end = s.end(); it != end; ++it) {
    if (*it != 'A') {
      result += *it;
    }
  }
  return result;
}
BENCHMARKFUNC(remove_6);

// 使用更好的算法
std::string remove_7(const std::string& s) {
  std::string result;
  result.reserve(s.length());
  for (size_t b = 0, i = b; b < s.length(); b = i + 1) {
    for (i = b; i < s.length(); ++i) {
      if (s[i] == 'A') {
        break;
      }
    }
    result.append(s, b, i - b);
  }
  return result;
}
BENCHMARKFUNC(remove_7);

// 删除修改
std::string remove_inplace(std::string s) {
  for (size_t i = 0; i < s.length();) {
    if (s[i] == 'A') {
      s.erase(i, 1);
    } else {
      ++i;
    }
  }
  return s;
}
BENCHMARKFUNC(remove_inplace);

BENCHMARK_MAIN();

image

  • remove_4 和 remove_5 的区别是参数传递改为引用。推测出现性能退化的原因,是remove_5中每次出现s,都会解引用,而在remove_4中不需要解引用,额外的开销导致性能下降。
  • remove_7 相较remove_6的性能下降,说明for循环嵌套对性能影响较大

bench mark

之前一直用 https://quick-bench.com/ 做小段代码性能测试,但是最近这个网站挂了,还是在本地跑 benchmark 靠谱。

安装bench mark

git clone https://github.com/google/benchmark.git
git clone https://github.com/google/googletest.git benchmark/googletest
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=RELEASE ../benchmark
make -j4
# 如果想全局安装就接着运行下面的命令
sudo make install

demo

测试代码

#include <chrono>
#include <iostream>
#include <thread>

#include <benchmark/benchmark.h>

struct S {
  long long a;
  long long b;
};

struct S2 {
  long long a;
  long long nonp[8];  // 占位,a、b 加载到不同的缓存行
  long long b;
};

void func1() {
  S s;
  std::thread t1([&]() {
    for (int i = 0; i < 100000; i++) {
      s.a++;
    }
  });
  std::thread t2([&]() {
    for (int i = 0; i < 100000; i++) {
      s.b++;
    }
  });
  t1.join();
  t2.join();
}

void func2() {
  S2 s;
  std::thread t1([&]() {
    for (int i = 0; i < 100000; i++) {
      s.a++;
    }
  });
  std::thread t2([&]() {
    for (int i = 0; i < 100000; i++) {
      s.b++;
    }
  });
  t1.join();
  t2.join();
}

static void BM_S(benchmark::State& state) {
  for (auto _ : state) {
    func1();
  }
}
BENCHMARK(BM_S);

static void BM_S2(benchmark::State& state) {
  for (auto _ : state) {
    func2();
  }
}
BENCHMARK(BM_S2);

BENCHMARK_MAIN();

编译(mac os)

clang++ -Wall -std=c++14 cache_line.cpp -I /usr/local/include -L /usr/local/lib -L /usr/lib -pthread -lbenchmark -o test

执行结果
image

posted @ 2023-03-09 18:45  我的娃会叫爸爸啦  阅读(51)  评论(0编辑  收藏  举报