此代码有什么未优化的?
What is unoptimized about this code?
我在面试街上为一个问题写了一个解决方案,这是问题描述:
https://www.interviewstreet.com/challenges/dashboard/#problem/4e91289c38bfd
这是他们给出的解决方案:
https://gist.github.com/1285119
这是我编码的解决方案:
#include<iostream>
#include <string.h>
using namespace std;
#define LOOKUPTABLESIZE 10000000
int popCount[2*LOOKUPTABLESIZE];
int main()
{
int numberOfTests = 0;
cin >> numberOfTests;
for(int test = 0;test<numberOfTests;test++)
{
int startingNumber = 0;
int endingNumber = 0;
cin >> startingNumber >> endingNumber;
int numberOf1s = 0;
for(int number=startingNumber;number<=endingNumber;number++)
{
if(number >-LOOKUPTABLESIZE && number < LOOKUPTABLESIZE)
{
if(popCount[number+LOOKUPTABLESIZE] != 0)
{
numberOf1s += popCount[number+LOOKUPTABLESIZE];
}
else
{
popCount[number+LOOKUPTABLESIZE] =__builtin_popcount (number);
numberOf1s += popCount[number+LOOKUPTABLESIZE];
}
}
else
{
numberOf1s += __builtin_popcount (number);
}
}
cout << numberOf1s << endl;
}
}
你能指出我的代码有什么问题吗?它只能通过3/10的测试。时间限制为 3 秒。
这段代码有什么未优化的?
算法。你正在循环
for(int number=startingNumber;number<=endingNumber;number++)
计算或查找每个 1 位的数量。这可能需要一段时间。
一个好的算法使用一些数学来计算0 <= k < n
时间内O(log n)
所有数字中的 1 位数。
这是一个在十进制扩展中计算 0 的实现,使其计数 1 位的修改应该不难。
在查看这样的问题时,您需要将其分解为简单的部分。
例如,假设你知道所有数字中有多少个 1[0, N]
(我们称之为ones(N)
),那么我们有:
size_t ones(size_t N) { /* magic ! */ }
size_t count(size_t A, size_t B) {
return ones(B) - (A ? ones(A - 1) : 0);
}
这种方法的优点是one
可能更容易编程该count
,例如使用递归。因此,第一次幼稚的尝试是:
// Naive
size_t naive_ones(size_t N) {
if (N == 0) { return 0; }
return __builtin_popcount(N) + naive_ones(N-1);
}
但这可能太慢了。即使简单地计算count(B, A)
的价值,我们也将计算naive_ones(A-1)
两次!
好在这里总有记忆辅助,转化相当微不足道:
size_t memo_ones(size_t N) {
static std::deque<size_t> Memo(1, 0);
for (size_t i = Memo.size(); i <= N; ++i) {
Memo.push_back(Memo[i-1] + __builtin_popcnt(i));
}
return Memo[N];
}
这很可能会有所帮助,但是内存方面的成本可能是......严重。呸。想象一下,对于计算ones(1,000,000)
我们将在 8 位计算机上占用 64MB 的内存!稀疏记忆可能会有所帮助(例如,仅每 8 或第 16 个计数记忆一次):
// count number of ones in (A, B]
static unoptimized_count(size_t A, size_t B) {
size_t result = 0;
for (size_t i = A + 1; i <= B; ++i) {
result += __builtin_popcount(i);
}
return result;
}
// something like this... be wary it's not tested.
size_t memo16_ones(size_t N) {
static std::vector<size_t> Memo(1, 0);
size_t const n16 = N - (N % 16);
for (size_t i = Memo.size(); i*16 <= n16; ++i) {
Memo.push_back(Memo[i-1] + unoptimized_count(16*(i-1), 16*i);
}
return Memo[n16/16] + unoptimized_count(n16, N);
}
然而,虽然它确实降低了内存成本,但它并没有解决主要的速度问题:我们至少必须使用__builtin_popcount
B 次!对于 B 的大值,这是一个杀手锏。
上述解决方案是机械的,不需要一丝思考。事实证明,面试与其说是编写代码,不如说是思考。
我们能比愚蠢地枚举所有整数直到B
更有效地解决这个问题吗?
让我们看看我们的大脑(相当神奇的模式机器)在考虑前几个条目时会选择什么:
N bin 1s ones(N)
0 0000 0 0
1 0001 1 1
2 0010 1 2
3 0011 2 4
4 0100 1 5
5 0101 2 7
6 0110 2 9
7 0111 3 12
8 1000 1 13
9 1001 2 15
10 1010 2 17
11 1011 3 20
12 1100 2 22
13 1101 3 25
14 1110 3 28
15 1111 3 32
注意到一个模式了吗?我做;)范围 8-15 的构建方式与 0-7 完全相同,但每行多一个 1 =>就像换位一样。这也是合乎逻辑的,不是吗?
因此,ones(15) - ones(7) = 8 + ones(7)
,ones(7) - ones(3) = 4 + ones(3)
和ones(1) - ones(0) = 1 + ones(0)
。
好吧,让我们把它做成一个公式:
- 提醒:根据定义
ones(N) = popcount(N) + ones(N-1)
(几乎) - 我们现在知道
ones(2**n - 1) - ones(2**(n-1) - 1) = 2**(n-1) + ones(2**(n-1) - 1)
我们做隔离ones(2**n)
,比较容易处理,注意popcount(2**n) = 1
:
- 重新组合:
ones(2**n - 1) = 2**(n-1) + 2*ones(2**(n-1) - 1)
- 使用定义:
ones(2**n) - 1 = 2**(n-1) + 2*ones(2**(n-1)) - 2
- 简化:
ones(2**n) = 2**(n-1) - 1 + 2*ones(2**(n-1))
,带ones(1) = 1
。
快速健全性检查:
1 = 2**0 => 1 (bottom)
2 = 2**1 => 2 = 2**0 - 1 + 2 * ones(1)
4 = 2**2 => 5 = 2**1 - 1 + 2 * ones(2)
8 = 2**3 => 13 = 2**2 - 1 + 2 * ones(4)
16 = 2**4 => 33 = 2**3 - 1 + 2 * ones(8)
看起来有效!
不过,我们还没有完全完成。A
和B
不一定是 2 的幂,如果我们必须从2**n
一直数到2**n + 2**(n-1)
那仍然是 O(N)!
另一方面,如果我们设法以 2 为底表示一个数字,那么我们应该能够利用我们新获得的公式。主要优点是表示中只有log2(N)位。
让我们举一个例子并了解它是如何工作的:13 = 8 + 4 + 1
1 -> 0001
4 -> 0100
8 -> 1000
13 -> 1101
。但是,计数不仅仅是总和:
ones(13) != ones(8) + ones(4) + ones(1)
让我们用"换位"策略来表达它:
ones(13) - ones(8) = ones(5) + (13 - 8)
ones(5) - ones(4) = ones(1) + (5 - 4)
好的,很容易做一些递归。
#include <cmath>
#include <iostream>
static double const Log2 = log(2);
// store ones(2**n) at P2Count[n]
static size_t P2Count[64] = {};
// Unfortunately, the conversion to double might lose some precision
// static size_t log2(size_t n) { return log(double(n - 1))/Log2 + 1; }
// __builtin_clz* returns the number of leading 0s
static size_t log2(size_t n) {
if (n == 0) { return 0; }
return sizeof(n) - __builtin_clzl(n) - 1;
}
static size_t ones(size_t n) {
if (n == 0) { return 0; }
if (n == 1) { return 1; }
size_t const lg2 = log2(n);
size_t const np2 = 1ul << lg2; // "next" power of 2
if (np2 == n) { return P2Count[lg2]; }
size_t const pp2 = np2 / 2; // "previous" power of 2
return ones(pp2) + ones(n - pp2) + (n - pp2);
} // ones
// reminder: ones(2**n) = 2**(n-1) - 1 + 2*ones(2**(n-1))
void initP2Count() {
P2Count[0] = 1;
for (size_t i = 1; i != 64; ++i) {
P2Count[i] = (1ul << (i-1)) - 1 + 2 * P2Count[i-1];
}
} // initP2Count
size_t count(size_t const A, size_t const B) {
if (A == 0) { return ones(B); }
return ones(B) - ones(A - 1);
} // count
还有一个演示:
int main() {
// Init table
initP2Count();
std::cout << "0: " << P2Count[0] << ", 1: " << P2Count[1] << ", 2: " << P2Count[2] << ", 3: " << P2Count[3] << "n";
for (size_t i = 0; i != 16; ++i) {
std::cout << i << ": " << ones(i) << "n";
}
std::cout << "count(7, 14): " << count(7, 14) << "n";
}
胜利!
注意:正如Daniel Fisher所指出的,这无法解释负数(但假设两个补码,可以从它们的正数中推断出来)。
- 这个C++编译器优化(在自身的实例上调用对象自己的构造函数)的名称是什么,它是如何工作的?
- 什么最适合用于优化SFML项目?
- 什么是使用 opencv::Mat 优化 c++ 矩阵计算
- 关于循环变量优化的标准合规行为是什么?
- 在 pthread 中运行 shell 命令的良好和优化方法是什么?
- "static_cast<易失性空隙>"对优化器意味着什么?
- 此代码有什么未优化的?
- 我必须做些什么才能在编译器优化的代码中调用函数
- 这种优化技术的名称是什么
- 在 C/C++ 中创建对象时编译器优化的边界是什么
- 编译器在程序集中优化代码时会做什么?即O2标志
- 编译器在尝试优化/内联我看起来微不足道但并非微不足道的 dtor 时搬起石头砸自己的脚,我做错了什么
- 模板与常规功能的优化:引擎盖下发生了什么?
- 这可以优化尾部调用吗?如果是这样,它没有发生的特殊原因是什么?
- 在展开的链表上运行大约需要40%的代码运行时间——有什么明显的方法可以优化它吗
- gcc会自动使用-j4吗?我能做些什么来优化我的编译吗
- LLVM编译器优化错误或什么
- 优化冒泡排序-我错过了什么
- 我可以使用什么算法来优化图像选择蒙版
- 在调用者内部扩展被调用者的指导原则是什么(内联-编译器优化)