我在下面的项目euler编码挑战中,代码给出的答案是正确的,但我不明白为什么它需要花费近一分钟的时间来运行。在使用筛子之前,它的完成时间相似。其他用户报告的时间低至毫秒。
我认为我在某处犯了一个基本错误......
// The sum of the primes below 10 is 2 + 3 + 5 + 7 = 17.
// Find the sum of all the primes below two million.
public static long Ex010()
{
var sum = 0L;
var sieve = new bool[2000000];
var primes = new List<int>(10000);
for (int i = 2; i < sieve.Length; i++)
{
if (sieve[i-1])
continue;
var isPrime = true;
foreach (var prime in primes)
{
if (i % prime == 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primes.Add(i);
sum += i;
for (var x = i * 2; x < sieve.Length; x += i) {
sieve[x-1] = true;
}
}
}
return sum;
}
唯一似乎缺少的是这种优化:
if (prime > Math.Sqrt(i))
break;
它将时间缩短到160毫秒。
最后点击,按照建议多次取出foreach。它现在12毫秒。最终解决方案:
public static long Ex010()
{
var sum = 0L;
var sieve = new bool[2000000];
for (int i = 2; i < sieve.Length; i++)
{
if (sieve[i-1])
continue;
sum += i;
for (var x = i * 2; x < sieve.Length; x += i) {
sieve[x-1] = true;
}
}
return sum;
}
答案 0 :(得分:3)
除筛子外,你正在进行试验 布尔数组已经告诉你数字是否为素数,因此根本不需要素数列表 你也可以通过筛选到极限的平方根来加速它 如果您还想保存一些内存,可以使用BitArray而不是布尔数组。
public static long Ex010()
{
const int Limit = 2000000;
int sqrt = (int)Math.Sqrt(Limit);
var sum = 0L;
var isComposite = new bool[Limit];
for (int i = 2; i < sqrt; i++) {
if (isComposite[i - 2])
continue;//This number is not prime, skip
sum += i;
for (var x = i * i; x < isComposite.Length; x += i) {
isComposite[x - 2] = true;
}
}
//Add the remaining prime numbers
for (int i = sqrt; i < Limit; i++) {
if (!isComposite[i - 2]) {
sum += i;
}
}
return sum;
}
答案 1 :(得分:2)
( tl;博士:在0.8毫秒内完成200万次,在1.25秒内完成20亿次分段;仅限分段赔率的SoE,预设,轮式跨越)
与往常一样,Euler任务#10的限制似乎旨在对ZX81,Apple [或C64]构成温和挑战,但在现代硬件上,您通常必须将限制乘以1000才能让事情变得有趣。或设置一个5秒的时间限制,并尝试查看可以超过欧拉限制的数量级......
Dennis_E的解决方案简单而有效,但我建议应用两项小改进,在不费力的情况下显着提升性能。
除了数字2之外的所有偶数都是复合数。如果你在需要时从空气中拉出数字2,那么你可以从筛子上取下所有偶数。这使工作负载和内存占用量减少一半,以便在一些地方写<<
或>>
的边际成本(在数字领域和位索引领域之间进行转换)的性能提高一倍。这通常被称为“仅有几率的筛子”。或者&#39;轮式表示mod 2&#39 ;;它具有额外的优势,它在很大程度上消除了防止索引溢出的需要。
在逐步浏览一系列数字时,跳过几个小素数(&#39;应用轮子&#39;)比在筛选过程中不同步幅疯狂地跳跃要容易得多。这个跳过仅涉及在不是所讨论的素数的倍数的连续数字之间应用循环的差异序列,例如4,2,4,2 ...用于跳过2和3的倍数(mod 6轮)或6, 4,2,4,2,4,6,2 ...用于跳过2,3和5的倍数。
mod 6车轮序列仅在两个数字之间交替,这可以通过使用适当的值进行异或来轻松实现。在仅有几率的筛子之上,距离减半,因此序列变为2,1,2,1 ......这种跳过减少了解码期间的工作1/3(对于步进模式3)和跳过的素数在筛分过程中也可以忽略。后者可以对筛分时间产生显着影响,因为最小的填料在筛分过程中会产生最大数量的杂交。
这里是一个简单的Eratosthenes筛子,并提出了两个建议。注意:这里和下面我通常使用C#/ .Net的流程并使用有符号整数,我通常会使用任何理智的语言中的无符号整数。那是因为我没有时间审查代码,因为使用无符号类型会导致性能影响(惩罚),比如编译器会突然忘记如何用一个常数替换除法逆转等等。
static long sum_small_primes_up_to (int n)
{
if (n < 7)
return (0xAA55200 >> (n << 2)) & 0xF;
int sqrt_n_halved = (int)Math.Sqrt(n) >> 1;
int max_bit = (int)(n - 1) >> 1;
var odd_composite = new bool[max_bit + 1];
for (int i = 5 >> 1; i <= sqrt_n_halved; ++i)
if (!odd_composite[i])
for (int p = (i << 1) + 1, j = p * p >> 1; j <= max_bit; j += p)
odd_composite[j] = true;
long sum = 2 + 3;
for (int i = 5 >> 1, d = 1; i <= max_bit; i += d, d ^= 3)
if (!odd_composite[i])
sum += (i << 1) + 1;
return sum;
}
第一个if
语句通过返回一个预先计算的数字列表的合适元素来处理小鱼苗(n在0..6中),它用于将所有特殊情况放在一个一举。所有其他出现的移位算子都是用于将数字域和指数域转换为仅有几率的筛子。
这几乎与我通常用于筛选小质数的代码相同,高达64K左右(数字最高为32位的潜在最小因素)。它在4.5毫秒内完成了Euler的2百万筹码,但是在它上面投出了更大的数字显示了它的致命弱点:它在很远的距离上进行了大量的跨越,它与现代的内存子系统相互作用,其中只有从现代内存子系统才能获得良好的访问速度缓存。当1级高速缓存的容量(通常为32 KiByte)显着超过时,性能显着下降,当超过L2和L3容量(通常为几兆字节)时,性能进一步下降。下降的锐度取决于计算机的质量(价格标签),当然......
以下是我的笔记本电脑上的一些时间:
# benchmark: small_v0 ...
sum up to 2 * 10^4: 21171191 in 0,03 ms
sum up to 2 * 10^5: 1709600813 in 0,35 ms // 11,0 times
sum up to 2 * 10^6: 142913828922 in 4,11 ms // 11,7 times
sum up to 2 * 10^7: 12272577818052 in 59,36 ms // 14,4 times
sum up to 2 * 10^8: 1075207199997334 in 1.225,19 ms // 20,6 times
sum up to 2 * 10^9: 95673602693282040 in 14.381,29 ms // 11,7 times
在中等范围内,时间增长远远超出预期因素11,然后事情再次稳定下来。
以下是如何将野兽加速一个数量级......
补救措施很容易:不是从范围的一端到另一端一直跨越每个素数 - 而是跨越存储空间 - 我们筛选缓存大小的条带范围,记住最终位置每个素数,以便下一轮可以继续前一轮停止的地方。如果我们在最后不需要一个大的坏筛子,那么我们可以在筛分后处理一个条带(提取其质数),然后丢弃其数据,重新使用筛子缓冲器进行下一个条带。两者都是分段筛分主题的变化,但在加工过程中对偏差的处理方式不同;当区别重要时,第一种方法(整个范围的大筛网)通常称为分段筛,后者称为迭代筛。条款&#39;移动&#39;或者&#39;滑动&#39;筛子可能有点适合后者,但应该避免,因为它们通常指的是一种完全不同的筛子(也称为deque筛),它们看似简单,但其性能至少降低了一个数量级。
这里是迭代筛的一个例子,这是我通常用于在给定范围[m,n]中筛选素数的函数的略微修改版本,就像在SPOJ的PRIMES1和PRINT中一样。这里参数m隐含为0,因此不需要传递。
通常,该函数采用一个负责处理原始筛子的接口(以及可能通过的任何丢失质数),并且可以查询处理器跳过的素数(&#39;解码器顺序和#) 39;)这样筛子可以在筛分期间忽略那些筛子。对于本次论述,为了简单起见,我已将其替换为代表。
因素素数被一个可能看起来有点熟悉的股票函数所筛选,并且我已经改变了筛子的逻辑来自&#39; is_composite&#39;到了&#39; not_composite&#39; (以及可以参与算术的基本类型),原因将在后面解释。 decoder_order
是解码器跳过的附加素数的数量(对于前面显示的函数,它将是1
,因为它在素数提取/求和期间,在轮子之上和之上跳过素数3的倍数素数2)。
const int SIEVE_BITS = 1 << 15; // L1 cache size, 1 usable bit per byte
delegate long sieve_sum_func (byte[] sieve, int window_base, int window_bits);
static long sum_primes_up_to (int n, sieve_sum_func sum_func, int decoder_order)
{
if (n < 7)
return 0xF & (0xAA55200 >> (n << 2));
n -= ~n & 1; // make odd (n can't be 0 here)
int sqrt_n = (int)Math.Sqrt(n);
var factor_primes = small_primes_up_to(sqrt_n).ToArray();
int first_sieve_prime_index = 1 + decoder_order; // skip wheel primes + decoder primes
int m = 7; // this would normally be factor_primes[first_sieve_prime_index] + 2
int bits_to_sieve = ((n - m) >> 1) + 1;
int sieve_bits = Math.Min(bits_to_sieve, SIEVE_BITS);
var sieve = new byte[sieve_bits];
var offsets = new int[factor_primes.Length];
int sieve_primes_end = first_sieve_prime_index;
long sum = 2 + 3 + 5; // wheel primes + decoder primes
for (int window_base = m; ; )
{
int window_bits = Math.Min(bits_to_sieve, sieve_bits);
int last_number_in_window = window_base - 1 + (window_bits << 1);
while (sieve_primes_end < factor_primes.Length)
{
int prime = factor_primes[sieve_primes_end];
int start = prime * prime, stride = prime << 1;
if (start > last_number_in_window)
break;
if (start < window_base)
start = (stride - 1) - (window_base - start - 1) % stride;
else
start -= window_base;
offsets[sieve_primes_end++] = start >> 1;
}
fill(sieve, window_bits, (byte)1);
for (int i = first_sieve_prime_index; i < sieve_primes_end; ++i)
{
int prime = factor_primes[i], j = offsets[i];
for ( ; j < window_bits; j += prime)
sieve[j] = 0;
offsets[i] = j - window_bits;
}
sum += sum_func(sieve, window_base, window_bits);
if ((bits_to_sieve -= window_bits) == 0)
break;
window_base += window_bits << 1;
}
return sum;
}
static List<int> small_primes_up_to (int n)
{
int upper_bound_on_pi = 32 + (n < 137 ? 0 : (int)(n / (Math.Log(n) - 1.083513)));
var result = new List<int>(upper_bound_on_pi);
if (n < 2)
return result;
result.Add(2); // needs to be pulled out of thin air because of the mod 2 wheel
if (n < 3)
return result;
result.Add(3); // needs to be pulled out of thin air because of the mod 3 decoder
int sqrt_n_halved = (int)Math.Sqrt(n) >> 1;
int max_bit = (n - 1) >> 1;
var odd_composite = new bool[max_bit + 1];
for (int i = 5 >> 1; i <= sqrt_n_halved; ++i)
if (!odd_composite[i])
for (int p = (i << 1) + 1, j = p * p >> 1; j <= max_bit; j += p)
odd_composite[j] = true;
for (int i = 5 >> 1, d = 1; i <= max_bit; i += d, d ^= 3)
if (!odd_composite[i])
result.Add((i << 1) + 1);
return result;
}
static void fill<T> (T[] array, int count, T value, int threshold = 16)
{
Trace.Assert(count <= array.Length);
int current_size = Math.Min(threshold, count);
for (int i = 0; i < current_size; ++i)
array[i] = value;
for (int half = count >> 1; current_size <= half; current_size <<= 1)
Buffer.BlockCopy(array, 0, array, current_size, current_size);
Buffer.BlockCopy(array, 0, array, current_size, count - current_size);
}
此处的筛选处理器相当于开头所示功能中使用的逻辑,以及可用于测量筛选时间的虚拟函数,无需进行任何解码,以进行比较:
static long prime_sum_null (byte[] sieve, int window_base, int window_bits)
{
return 0;
}
static long prime_sum_v0 (byte[] sieve, int window_base, int window_bits)
{
long sum = 0;
int i = window_base % 3 == 0 ? 1 : 0;
int d = 3 - (window_base + 2 * i) % 3;
for ( ; i < window_bits; i += d, d ^= 3)
if (sieve[i] == 1)
sum += window_base + (i << 1);
return sum;
}
这个函数需要执行一些模数魔法,以便在mod 2筛上与mod 3序列同步;早期的函数不需要这样做,因为它的起点是固定的,而不是参数。以下是时间安排:
# benchmark: iter_v0 ...
sum up to 2 * 10^4: 21171191 in 0,04 ms
sum up to 2 * 10^5: 1709600813 in 0,28 ms // 7,0 times
sum up to 2 * 10^6: 142913828922 in 2,42 ms // 8,7 times
sum up to 2 * 10^7: 12272577818052 in 22,11 ms // 9,1 times
sum up to 2 * 10^8: 1075207199997334 in 223,67 ms // 10,1 times
sum up to 2 * 10^9: 95673602693282040 in 2.408,06 ms // 10,8 times
相当不同,n&#39; est-ce pas?但我们还没有完成。
现代处理器喜欢简单易懂的东西;如果没有正确预测分支,那么CPU在额外的周期中会产生很大的罚款,以便刷新和重新填充指令管道。不幸的是,解码循环不是很容易预测,因为素数在我们在这里讨论的低数字范围内相当密集:
if (!odd_composite[i])
++count;
如果素数之间的非素数的平均数乘以加法的成本小于错误预测的分支的惩罚那么下面的陈述应该更快:
count += sieve[i];
这解释了为什么我将筛子的逻辑与正常相比颠倒了,因为&#39; is_composite&#39;语义学我必须做
count += 1 ^ odd_composite[i];
规则是将所有东西从可以拉出的内循环中拉出来,这样我就可以在开始之前将1 ^ x
简单地应用到整个数组中。
然而,欧拉希望我们总结素数而不是计算它们。这可以通过将值1
转换为所有1位的掩码(在ANDing时保留所有内容)并将0归零任何值来以类似的方式完成。这与CMOV指令类似,不同之处在于它甚至可以在最老的CPU上工作,并且不需要相当不错的编译器:
static long prime_sum_v1 (byte[] sieve, int window_base, int window_bits)
{
long sum = 0;
int i = window_base % 3 == 0 ? 1 : 0;
int d = 3 - (window_base + 2 * i) % 3;
for ( ; i < window_bits; i += d, d ^= 3)
sum += (0 - sieve[i]) & (window_base + (i << 1));
return sum;
}
结果:
# benchmark: iter_v1 ...
sum up to 2 * 10^4: 21171191 in 0,10 ms
sum up to 2 * 10^5: 1709600813 in 0,36 ms // 3,6 times
sum up to 2 * 10^6: 142913828922 in 1,88 ms // 5,3 times
sum up to 2 * 10^7: 12272577818052 in 13,80 ms // 7,3 times
sum up to 2 * 10^8: 1075207199997334 in 157,39 ms // 11,4 times
sum up to 2 * 10^9: 95673602693282040 in 1.819,05 ms // 11,6 times
现在,有点矫枉过正:具有完全展开的轮模15的解码器(展开可以解锁一些指令级并行的保留)。
static long prime_sum_v5 (byte[] sieve, int window_base, int window_bits)
{
Trace.Assert(window_base % 2 == 1);
int count = 0, sum = 0;
int residue = window_base % 30;
int phase = UpperIndex[residue];
int i = (SpokeValue[phase] - residue) >> 1;
// get into phase for the unrolled code (which is based on phase 0)
for ( ; phase != 0 && i < window_bits; i += DeltaDiv2[phase], phase = (phase + 1) & 7)
{
int b = sieve[i]; count += b; sum += (0 - b) & i;
}
// process full revolutions of the wheel (anchored at phase 0 == residue 1)
for (int e = window_bits - (29 >> 1); i < e; i += (30 >> 1))
{
int i0 = i + ( 1 >> 1), b0 = sieve[i0]; count += b0; sum += (0 - b0) & i0;
int i1 = i + ( 7 >> 1), b1 = sieve[i1]; count += b1; sum += (0 - b1) & i1;
int i2 = i + (11 >> 1), b2 = sieve[i2]; count += b2; sum += (0 - b2) & i2;
int i3 = i + (13 >> 1), b3 = sieve[i3]; count += b3; sum += (0 - b3) & i3;
int i4 = i + (17 >> 1), b4 = sieve[i4]; count += b4; sum += (0 - b4) & i4;
int i5 = i + (19 >> 1), b5 = sieve[i5]; count += b5; sum += (0 - b5) & i5;
int i6 = i + (23 >> 1), b6 = sieve[i6]; count += b6; sum += (0 - b6) & i6;
int i7 = i + (29 >> 1), b7 = sieve[i7]; count += b7; sum += (0 - b7) & i7;
}
// clean up leftovers
for ( ; i < window_bits; i += DeltaDiv2[phase], phase = (phase + 1) & 7)
{
int b = sieve[i]; count += b; sum += (0 - b) & i;
}
return (long)window_base * count + ((long)sum << 1);
}
正如您所看到的,为了使编译器更容易,我执行了一些强度降低。我没有对window_base + (i << 1)
进行求和,而是分别对i
和1
进行求和,并在函数末尾执行一次剩余的计算。
时序:
# benchmark: iter_v5(1) ...
sum up to 2 * 10^4: 21171191 in 0,01 ms
sum up to 2 * 10^5: 1709600813 in 0,11 ms // 9,0 times
sum up to 2 * 10^6: 142913828922 in 1,01 ms // 9,2 times
sum up to 2 * 10^7: 12272577818052 in 11,52 ms // 11,4 times
sum up to 2 * 10^8: 1075207199997334 in 130,43 ms // 11,3 times
sum up to 2 * 10^9: 95673602693282040 in 1.563,10 ms // 12,0 times
# benchmark: iter_v5(2) ...
sum up to 2 * 10^4: 21171191 in 0,01 ms
sum up to 2 * 10^5: 1709600813 in 0,09 ms // 8,7 times
sum up to 2 * 10^6: 142913828922 in 1,03 ms // 11,3 times
sum up to 2 * 10^7: 12272577818052 in 10,34 ms // 10,0 times
sum up to 2 * 10^8: 1075207199997334 in 121,08 ms // 11,7 times
sum up to 2 * 10^9: 95673602693282040 in 1.468,28 ms // 12,1 times
第一组时序是decoder_order == 1
(即没有告诉筛子额外跳过的素数),以便与其他解码器版本直接比较。第二组是decoder_order == 2
,这意味着筛子可以跳过5号的交叉点。这里是空时间(基本上没有解码时间的筛选时间),把事情放到一个透视图中:
# benchmark: iter_null(1) ...
sum up to 2 * 10^8: 10 in 94,74 ms // 11,4 times
sum up to 2 * 10^9: 10 in 1.194,18 ms // 12,6 times
# benchmark: iter_null(2) ...
sum up to 2 * 10^8: 10 in 86,05 ms // 11,9 times
sum up to 2 * 10^9: 10 in 1.109,32 ms // 12,9 times
这表明解码器的工作已经将解码时间减少了20亿,从1.21秒减少到0.35秒,这没什么可打喷嚏的。筛分也可以实现类似的加速,但这远不如解码那么容易。
最后,一种有时可以提供显着加速的技术(特别是对于打包的位图和/或更高阶的轮子)是在开始一轮筛分之前在筛子上喷射罐头模式,这样筛子看起来就好像它一样已经被一些小素数筛分过了。这通常被称为presieving。在目前的情况下,加速是微不足道的(甚至不是20%),但是我展示了它,因为它是一种在一个工具箱中使用的有用技术。
注意:我已经从另一个Euler项目中删除了预设逻辑,因此它并不适合我为本文编写的代码。但它应该足够好地证明这项技术。
const byte CROSSED_OFF = 0; // i.e. composite
const byte NOT_CROSSED = 1 ^ CROSSED_OFF; // i.e. not composite
const int SIEVE_BYTES = SIEVE_BITS; // i.e. 1 usable bit per byte
internal readonly static byte[] TinyPrimes = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 };
internal readonly static int m_wheel_order = 3; // == number of wheel primes
internal static int m_presieve_level = 0; // == number of presieve primes
internal static int m_presieve_modulus = 0;
internal static byte[] m_presieve_pattern;
internal static void set_presieve_level (int presieve_primes)
{
m_presieve_level = Math.Max(0, presieve_primes);
m_presieve_modulus = 1;
for (int i = m_wheel_order; i < m_wheel_order + presieve_primes; ++i)
m_presieve_modulus *= TinyPrimes[i];
// the pattern needs to provide SIEVE_BYTES bytes for every residue of the modulus
m_presieve_pattern = new byte[m_presieve_modulus + SIEVE_BYTES - 1];
var pattern = m_presieve_pattern;
int current_size = 1;
pattern[0] = NOT_CROSSED;
for (int i = m_wheel_order; i < m_wheel_order + presieve_primes; ++i)
{
int current_prime = TinyPrimes[i];
int new_size = current_size * current_prime;
// keep doubling while possible
for ( ; current_size * 2 <= new_size; current_size *= 2)
Buffer.BlockCopy(pattern, 0, pattern, current_size, current_size);
// copy rest, if any
Buffer.BlockCopy(pattern, 0, pattern, current_size, new_size - current_size);
current_size = new_size;
// mark multiples of the current prime
for (int j = current_prime >> 1; j < current_size; j += current_prime)
pattern[j] = CROSSED_OFF;
}
for (current_size = m_presieve_modulus; current_size * 2 <= pattern.Length; current_size *= 2)
Buffer.BlockCopy(pattern, 0, pattern, current_size, current_size);
Buffer.BlockCopy(pattern, 0, pattern, current_size, pattern.Length - current_size);
}
对于快速测试,您可以按照以下方式破解筛选功能:
- int first_sieve_prime_index = 1 + decoder_order; // skip wheel primes + decoder primes
+ int first_sieve_prime_index = 1 + decoder_order + m_presieve_level; // skip wheel primes + decoder primes
加
- long sum = 2 + 3 + 5; // wheel primes + decoder primes
+ long sum = 2 + 3 + 5; // wheel primes + decoder primes
+
+ for (int i = 0; i < m_presieve_level; ++i)
+ sum += TinyPrimes[m_wheel_order + i];
加
- fill(sieve, window_bits, (byte)1);
+ if (m_presieve_level == 0)
+ fill(sieve, window_bits, (byte)1);
+ else
+ Buffer.BlockCopy(m_presieve_pattern, (window_base >> 1) % m_presieve_modulus, sieve, 0, window_bits);
和
set_presieve_level(4) // 4 and 5 work well
在静态构造函数或Main()中。
这样您可以使用m_presieve_level打开和关闭预设。但是,在调用set_presieve_level(0)
之后,BlockCopy也能正常工作,因为模数为1. m_wheel_order
应该反映实际的轮次顺序(= 1)加上解码器顺序;它目前设置为3,因此它只适用于2级的v5解码器。
时序:
# benchmark: iter_v5(2) pre(7) ...
sum up to 2 * 10^4: 21171191 in 0,02 ms
sum up to 2 * 10^5: 1709600813 in 0,08 ms // 4,0 times
sum up to 2 * 10^6: 142913828922 in 0,78 ms // 9,6 times
sum up to 2 * 10^7: 12272577818052 in 8,78 ms // 11,2 times
sum up to 2 * 10^8: 1075207199997334 in 98,89 ms // 11,3 times
sum up to 2 * 10^9: 95673602693282040 in 1.245,19 ms // 12,6 times
sum up to 2^31 - 1: 109930816131860852 in 1.351,97 ms