这里记录一下一些简短有效的代码,值得参考。
template
static inline char* align_for(char* ptr)
{
const std::size_t alignment = std::alignment_of::value;
return ptr + (alignment - (reinterpret_cast(ptr) % alignment)) % alignment;
}
这个是获得比最小的不小于x
的2的n次幂。
template
static inline T ceil_to_pow_2(T x)
{
static_assert(std::is_integral::value && !std::numeric_limits::is_signed, "ceil_to_pow_2 is intended to be used only with unsigned integer types");
// Adapted from http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
--x;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
for (std::size_t i = 1; i < sizeof(T); i <<= 1) {
x |= x >> (i << 3);
}
++x;
return x;
}
避免判断分支的stall
,于是直接用标志位来计算了。出处见pbrt。
int a[3];
int bits = ((a[0] < a[1]) << 2) + ((a[0] < a[2]) << 1) + (a[1] < a[2]
const int smallest[8] = [2, 1, 2, 1, 2, 2, 0, 0]
int smallest_v = a[smallest[bits]]
int bits = ((a[0] > a[1]) << 2) + ((a[0] > a[2]) << 1) + (a[1] > a[2]
const int biggest[8] = [2, 1, 2, 1, 2, 2, 0, 0]
int biggest_v = a[biggest[bits]]
Intel Haswell 架构的 DIV 32位除法指令的延迟(latency)是 28 个周期,吞吐率是 10 个周期。作为比较,同一架构下 MUL 32位乘法指令的延迟只是 4 个周期,吞吐率只是半个周期。所以对于常见的除以10的计算更好的方法是转化为乘法。
int a = 3728463;
int b = a/10;
int64_t c = 3435973837;
int d = static_cast((static_cast(a) * c)>>35)
assert( b == d)
这里的b == d
之所以会成立是因为c>>35 = 0.10000000058
,在精度范围内没有误差。编译器内部维护了很多小整数的除法优化表,对于常量整数的除法都可以转换为64位乘法然后右移的方式。所以下面的这段代码的两个除法热点就可以被优化了:
uint32_t p1 = /*...*/;
int kappa = 10;
uint32_t div = 1000000000;
while (kappa > 0) {
d = p1 / div; // 第一个除法
if (d || *len)
buffer[(*len)++] = static_cast('0' + static_cast(d));
p1 %= div;
kappa--;
div /= 10; // 第二个除法
// ...
}
这里的div
并不是常量,在循环过程中会变,转化为常量之后就可以很大程度的提高性能。
while (kappa > 0) {
uint32_t d = 0;
switch (kappa) {
case 9: d = p1 / 100000000; p1 %= 100000000; break;
case 8: d = p1 / 10000000; p1 %= 10000000; break;
case 7: d = p1 / 1000000; p1 %= 1000000; break;
case 6: d = p1 / 100000; p1 %= 100000; break;
case 5: d = p1 / 10000; p1 %= 10000; break;
case 4: d = p1 / 1000; p1 %= 1000; break;
case 3: d = p1 / 100; p1 %= 100; break;
case 2: d = p1 / 10; p1 %= 10; break;
case 1: d = p1; p1 = 0; break;
default:;
}
if (d || *len)
buffer[(*len)++] = static_cast('0' + static_cast(d));
kappa--;
// ...
}
这里不需要担心case
里的两个除法,现代的编译器针对取模运算和除法运算右边的数相同时,会优化为一条指令。相关资料来自于Milo 的blog
utf8 的字节编码是变长的,实际使用过程中经常需要parse
之后转变为定长的来处理。这里的热点就是扫描分割边长编码, 这里我们为了简单起见只处理最长为4字节的变长编码。最简单的实现是这样的:
vector utf8_to_uint(const string& text) const
{
unsigned char u, v, w, x, y, z;
vector utf8_result;
int num_chars = 0;
uint32_t num_bytes = text.length();
long iii = 0;
while (iii < num_bytes)
{
uint32_t cur_utf8_char = 0;
z = text[iii];
if (z <= 127)
{
cur_utf8_char = z;
}
if (z >= 192 && z <= 223)
{
iii++;
y = text[iii];
cur_utf8_char = (z - 192) * 64 + (y - 128);
}
if (z >= 224 && z <= 239)
{
iii++; y = text[iii];
iii++; x = text[iii];
cur_utf8_char = (z - 224) * 4096 + (y - 128) * 64 + (x - 128);
}
if ((240 <= z)
{
iii++; y = text[iii];
iii++; x = text[iii];
iii++; w = text[iii];
cur_utf8_char = (z - 240) * 262144 + (y - 128) * 4096 + (x - 128) * 64 + (w - 128);
}
utf8_result.push_back(cur_utf8_char);
iii++;
}
return utf8_result;
}
下面有一些比较好的代码可以参考:
do {
c = pStr[0]; if (globals::s_parse_flags[c] & 1)
{ ++pStr; break; }
c = pStr[1]; if (globals::s_parse_flags[c] & 1)
{ pStr += 2; break; }
c = pStr[2]; if (globals::s_parse_flags[c] & 1)
{ pStr += 3; break; }
c = pStr[3];
pStr += 4;
} while (!(globals::s_parse_flags[c] & 1));
这段代码粗看起来很诡异,事实上他利用了循环展开,直接让cpu
有了一次性装载四个字节并判断的能力。对于第一个版本的改动就是对z <= 127
进行循环展开,也试图判断四个字节。
在很多lex
程序中都需要跳过空白字符,例如\t \n \r
这四个。简单的实现会涉及到很多的branch
,例如下面的代码:
template
void SkipWhitespace(InputStream& is) {
internal::StreamLocalCopy copy(is);
InputStream& s(copy.s);
while (s.Peek() == ' ' ||
s.Peek() == '\n' ||
s.Peek() == '\r' ||
s.Peek() == '\t')
{
s.Take();
}
}
此时可以利用intel sse4.2 pcmpistrm
指令来加速比较,这个指令可以一次对一组16个字符与另外一组字符做比较,最多支持1616, 虽然对于空白字符来说我们只有16 4, 不过也算加速很大了。具体实现如下:
inline const char *SkipWhitespace_SIMD(const char* p) {
// ... 非对齐处理
static const char whitespace[16] = " \n\r\t";
const __m128i w = _mm_load_si128((const __m128i *)&whitespace[0]);
for (;; p += 16) {
const __m128i s = _mm_load_si128((const __m128i *)p);
const unsigned r = _mm_cvtsi128_si32(_mm_cmpistrm(w, s,
_SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY |
_SIDD_BIT_MASK | _SIDD_NEGATIVE_POLARITY));
if (r != 0) { // some of characters is non-whitespace
#ifdef _MSC_VER // Find the index of first non-whitespace
unsigned long offset;
_BitScanForward(&offset, r);
return p + offset;
#else
return p + __builtin_ffs(r) - 1;
#endif
}
这里还有一个可以优化的地方,就是第一个字符串已经是非空白字符了,此时再去调用sse
其实消耗很大,所以可以优化第一个字节的判断:
inline const char *SkipWhitespace_SIMD(const char* p) {
// Fast return for single non-whitespace
if (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
++p;
else
return p;
// ...
}
核心思想是生成二进制位对应的表,然后根据这个数字的leading zero count 来读表判断多少位。
array, 64> generate_delimit()
// 生成分隔数组
{
array, 64> result = {};
int _pre = 1;
uint64_t temp_2 = 0;
uint64_t temp_10 = 10;
for (int i = 0; i < 64; i++)
{
temp_2 = (temp_2 << 1) + 1;
uint64_t current_bit = ceil(log10(temp_2));
if (current_bit <= _pre)
{
result[i][0] = temp_2;
result[i][1] = _pre;
}
else
{
result[i][0] = temp_10 - 1;
result[i][1] = _pre;
_pre = current_bit;
temp_10 = temp_10 * 10;
}
}
return result;
};
uint32_t digits10(uint64_t v)
{
const static array, 64> bit_table = generate_delimit();
if (v == 0)
{
return 1;
}
auto cur_index = 63 - __lzcnt64(v);// 这行指令是msvc特有的 其他平台的名字不同
return bit_table[cur_index][1] + (v > bit_table[cur_index][0]);
}