int, короткая, байтовая производительность в обратных циклах for

(фон: Почему я должен используйте int вместо byte или short в C#)

Чтобы удовлетворить свое собственное любопытство относительно плюсов и минусов использования целого числа соответствующего размера по сравнению с оптимизированным целым числом, я написал следующий код, который подкрепил то, что я ранее считал верным в отношении производительности int в .Net (и что объясняется в ссылке выше), который что он оптимизирован для производительности int, а не для коротких или байтовых.

DateTime t;
long a, b, c;

t = DateTime.Now;
for (int index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}           
a = DateTime.Now.Ticks - t.Ticks;

t = DateTime.Now;
for (short index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
        
b=DateTime.Now.Ticks - t.Ticks;

t = DateTime.Now;           
for (byte index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
c=DateTime.Now.Ticks - t.Ticks;

Console.WriteLine(a.ToString());
Console.WriteLine(b.ToString());
Console.WriteLine(c.ToString());

Это дает примерно согласованные результаты в области...

~950000

~2000000

~1700000

Что соответствует тому, что я ожидал увидеть.

Однако, когда я пытаюсь повторить циклы для каждого типа данных, как это...

t = DateTime.Now;
for (int index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
for (int index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
for (int index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
a = DateTime.Now.Ticks - t.Ticks;

Цифры больше похожи...

~4500000

~3100000

~300000

Что я нахожу озадачивающим. Может ли кто-нибудь предложить объяснение?

ПРИМЕЧАНИЕ. В интересах сравнения я ограничил количество циклов до 127 из-за диапазона значений типа byte. Кроме того, это акт любопытства, а не микрооптимизация производственного кода.


person gingerbreadboy    schedule 07.04.2010    source источник
comment
byte имеет диапазон 0-255. Это не подписанный тип данных.   -  person Adam Robinson    schedule 07.04.2010
comment
Кроме того, класс DateTime не подходит для низкоуровневого профилирования производительности. Используйте System.Diagnostics.Stopwatch.   -  person Adam Robinson    schedule 07.04.2010
comment
@Aaronaught, Джон: Спасибо за решение. У меня есть некоторые пояснения... индекс ‹ 255/127;... В этом коде 255/127 всегда является типом данных Byte/Short/Int. Или .Net IL изменит свой тип данных 255/127 на индексный тип данных для соответствующие циклы? Мы можем объявить константу для соответствующего типа данных цикла for и проверить?   -  person Thulasiram    schedule 08.06.2017


Ответы (8)


Прежде всего, это не .NET, оптимизированный для int производительности, а машина, которая оптимизирована, потому что 32-битный размер слова (если вы не используете x64, в этом случае это long или 64-битный ).

Во-вторых, вы пишете в консоль внутри каждого цикла - это будет намного дороже, чем увеличение и проверка счетчика циклов, поэтому здесь вы не измеряете ничего реалистичного.

В-третьих, byte имеет диапазон до 255, поэтому вы можете выполнить цикл 254 раза (если вы попытаетесь сделать 255, он переполнится, и цикл никогда не закончится, но вам не нужно останавливаться на 128).

В-четвертых, вы не выполняете близко количество итераций для профилирования. Итерировать тугой цикл 128 или даже 254 раза бессмысленно. Что вам нужно сделать, так это поместить цикл byte/short/int в другой цикл, который выполняет гораздо большее количество итераций, скажем, 10 миллионов, и проверить результаты этого.

Наконец, использование DateTime.Now в расчетах приведет к некоторому временному «шуму» при профилировании. Рекомендуется (и проще) использовать класс Stopwatch. вместо.

В итоге требуется много изменений, прежде чем он станет действительным тестом производительности.


Вот что я считаю более точной тестовой программой:

class Program
{
    const int TestIterations = 5000000;

    static void Main(string[] args)
    {
        RunTest("Byte Loop", TestByteLoop, TestIterations);
        RunTest("Short Loop", TestShortLoop, TestIterations);
        RunTest("Int Loop", TestIntLoop, TestIterations);
        Console.ReadLine();
    }

    static void RunTest(string testName, Action action, int iterations)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < iterations; i++)
        {
            action();
        }
        sw.Stop();
        Console.WriteLine("{0}: Elapsed Time = {1}", testName, sw.Elapsed);
    }

    static void TestByteLoop()
    {
        int x = 0;
        for (byte b = 0; b < 255; b++)
            ++x;
    }

    static void TestShortLoop()
    {
        int x = 0;
        for (short s = 0; s < 255; s++)
            ++x;
    }

    static void TestIntLoop()
    {
        int x = 0;
        for (int i = 0; i < 255; i++)
            ++x;
    }
}

Это запускает каждый цикл внутри гораздо большего цикла (5 миллионов итераций) и выполняет очень простую операцию внутри цикла (увеличивает переменную). Результаты для меня были:

Байтовый цикл: истекшее время = 00:00:03.8949910
Короткий цикл: истекшее время = 00:00:03.9098782
Внутренний цикл: истекшее время = 00:00:03.2986990

Так что особой разницы нет.

Кроме того, убедитесь, что вы профилируете в режиме выпуска, многие забывают и тестируют в режиме отладки, что будет значительно менее точным.

person Aaronaught    schedule 07.04.2010
comment
О, спасибо, я никогда раньше не пробовал профилировать свой код. Хорошие моменты, приняты к сведению :) - person gingerbreadboy; 07.04.2010
comment
@Jon: клянусь, я не копировал твое. :П - person Aaronaught; 07.04.2010
comment
О, я совсем не об этом думал. Просто позабавился. - person Jon Skeet; 07.04.2010
comment
Не так уж много, чтобы разделить ответы, так что всенародное голосование берет это за нос. Здоровья, ребята. - person gingerbreadboy; 07.04.2010
comment
›В-третьих, байт имеет диапазон до 255, так что вы можете выполнить цикл 254 раза. Это меня беспокоит; если вы используете цикл for, поддерживаемый байтом, на самом деле невозможно, чтобы он перебирал все возможные значения байта, и вам нужно было бы использовать больший тип данных? Это только кажется глупым, но я понимаю, почему это происходит. - person Kyle Baran; 09.09.2014

Большая часть этого времени, вероятно, уходит на запись в консоль. Попробуйте сделать что-нибудь другое в цикле...

Кроме того:

  • Использование DateTime.Now — плохой способ измерения времени. Вместо этого используйте System.Diagnostics.Stopwatch
  • Как только вы избавитесь от вызова Console.WriteLine, цикл из 127 итераций будет слишком коротким для измерения. Вам нужно запустить цикл много раз, чтобы получить разумное измерение.

Вот мой ориентир:

using System;
using System.Diagnostics;

public static class Test
{    
    const int Iterations = 100000;

    static void Main(string[] args)
    {
        Measure(ByteLoop);
        Measure(ShortLoop);
        Measure(IntLoop);
        Measure(BackToBack);
        Measure(DelegateOverhead);
    }

    static void Measure(Action action)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        Stopwatch sw = Stopwatch.StartNew();
        for (int i = 0; i < Iterations; i++)
        {
            action();
        }
        sw.Stop();
        Console.WriteLine("{0}: {1}ms", action.Method.Name,
                          sw.ElapsedMilliseconds);
    }

    static void ByteLoop()
    {
        for (byte index = 0; index < 127; index++)
        {
            index.ToString();
        }
    }

    static void ShortLoop()
    {
        for (short index = 0; index < 127; index++)
        {
            index.ToString();
        }
    }

    static void IntLoop()
    {
        for (int index = 0; index < 127; index++)
        {
            index.ToString();
        }
    }

    static void BackToBack()
    {
        for (byte index = 0; index < 127; index++)
        {
            index.ToString();
        }
        for (short index = 0; index < 127; index++)
        {
            index.ToString();
        }
        for (int index = 0; index < 127; index++)
        {
            index.ToString();
        }
    }

    static void DelegateOverhead()
    {
        // Nothing. Let's see how much
        // overhead there is just for calling
        // this repeatedly...
    }
}

И результаты:

ByteLoop: 6585ms
ShortLoop: 6342ms
IntLoop: 6404ms
BackToBack: 19757ms
DelegateOverhead: 1ms

(Это на нетбуке - корректируйте количество итераций, пока не получится что-то толковое :)

Это, кажется, показывает, что в основном нет существенной разницы, какой тип вы используете.

person Jon Skeet    schedule 07.04.2010
comment
но все циклы записываются в консоль одинаковое количество раз, т.е. 127 x n-циклов - person gingerbreadboy; 07.04.2010
comment
хотя я думаю, что int.toString() может занять больше времени, чем byte.toString(), может быть? - person gingerbreadboy; 07.04.2010
comment
@runrunraygun: Console.WriteLine — это асинхронная операция с ненадежным временем выполнения. Хотя маловероятно, что это сильно повлияет на ваши результаты, используйте что-то более надежное. Кроме того, int.ToString() — это не та же функция, что и byte.ToString(), поэтому вы не выполняете одно и то же действие в каждом цикле. - person Adam Robinson; 07.04.2010
comment
@Adam: я сохранил различие между int.ToString и byte.ToString() в своем тесте, но удалил вызов Console.WriteLine. Итак, это тестирование зацикливания с помощью int и преобразование int в строку с зацикливанием с коротким и преобразованием короткого в строку и т. д. - person Jon Skeet; 07.04.2010
comment
Я не могу себе представить, что это на самом деле важно, но на самом деле нет необходимости иметь дело с переменной цикла в циклах измерения, не так ли? С тем же успехом вы могли бы иметь переменную int в каждой функции, для которой вы вызываете ToString() внутри цикла. - person Adam Robinson; 07.04.2010
comment
Что делать, если цикл был пуст без какой-либо операции == удалить index.ToString();? Кажется, тогда результаты другие. - person Santhos; 02.03.2016
comment
@Santhos: Тогда я ожидаю, что JIT-компилятор сможет полностью оптимизировать цикл ... - person Jon Skeet; 02.03.2016
comment
@JonSkeet это не похоже, потому что вы все еще тратите время, когда пытаетесь это сделать. - person Santhos; 02.03.2016
comment
@Santhos: я говорил по крайней мере теоретически. По сути, JIT-компилятор решает оптимизировать детали реализации. Конечно, выполнение всего со значением может повлиять на результат — может быть, byte.ToString() быстрее, чем int.ToString()... (я не уверен, что это тот тест, который я бы использовал сейчас...) - person Jon Skeet; 02.03.2016

Просто из любопытства я немного модифицировал программу от Aaronaught и скомпилировал ее как в режиме x86, так и в режиме x64. Странно, Int работает намного быстрее в x64:

x86

Цикл байтов: истекшее время = 00:00:00.8636454
Короткий цикл: истекшее время = 00:00:00.8795518
Короткий цикл US: истекшее время = 00:00:00.8630357
Внутренний цикл: истекшее время = 00: 00:00.5184154
Цикл UInt: Истекшее время = 00:00:00.4950156
Длинный цикл: Истекшее время = 00:00:01.2941183
Цикл ULong: Истекшее время = 00:00:01.3023409

x64

Цикл байтов: истекшее время = 00:00:01.0646588
Короткий цикл: истекшее время = 00:00:01.0719330
Короткий цикл US: истекшее время = 00:00:01.0711545
Внутренний цикл: истекшее время = 00: 00:00.2462848
Цикл UInt: Истекшее время = 00:00:00.4708777
Длинный цикл: Истекшее время = 00:00:00.5242272
Цикл ULong: Истекшее время = 00:00:00.5144035

person ialiashkevich    schedule 22.05.2012

Я опробовал две приведенные выше программы, так как они выглядели так, как будто они будут давать разные и, возможно, противоречивые результаты на моей машине разработки.

Результаты тестовой системы Aaronaughts

Short Loop: Elapsed Time = 00:00:00.8299340
Byte Loop: Elapsed Time = 00:00:00.8398556
Int Loop: Elapsed Time = 00:00:00.3217386
Long Loop: Elapsed Time = 00:00:00.7816368

инт намного быстрее

Выходы от Джона

ByteLoop: 1126ms
ShortLoop: 1115ms
IntLoop: 1096ms
BackToBack: 3283ms
DelegateOverhead: 0ms

ничего в этом

У Джона есть большая фиксированная константа вызова tostring в результатах, которая может скрывать возможные преимущества, которые могли бы возникнуть, если бы работа, проделанная в цикле, была меньше. Aaronaught использует 32-битную ОС, которая, похоже, не так сильно выигрывает от использования целых чисел, как установка x64, которую я использую.

Аппаратное и программное обеспечение. Результаты были собраны на процессоре Core i7 975 с тактовой частотой 3,33 ГГц с отключенным режимом Turbo и привязкой ядер, установленной для уменьшения влияния других задач. Все настройки производительности установлены на максимум, а сканер вирусов / ненужные фоновые задачи приостановлены. Windows 7 x64 Ultimate с 11 ГБ оперативной памяти и очень небольшой активностью ввода-вывода. Запустите конфигурацию выпуска, встроенную в версию 2008, без подключенного отладчика или профилировщика.

Повторяемость Первоначально повторяется 10 раз, меняя порядок выполнения для каждого теста. Вариация была незначительной, поэтому я опубликовал только свой первый результат. При максимальной загрузке ЦП соотношение времени выполнения оставалось постоянным. Повторные запуски на нескольких блейд-серверах x64 xp xeon дают примерно одинаковые результаты с учетом поколения ЦП и ГГц.

Профилирование Профилировщик Redgate / Jetbrains / Slimtune / CLR и мой собственный профилировщик показывают, что результаты верны.

Отладка сборки. Использование параметров отладки в VS дает стабильные результаты, как у Ааронаута.

person Steve    schedule 07.04.2010
comment
У меня коробка x64. Это довольно аномальный результат для первого теста — похоже, что версии short и byte заняли намного больше времени, чем должны были, в то время как версия int была очень близка к моей. Вы несколько раз проводили тест? У вас параллельно что-то еще работало? - person Aaronaught; 08.04.2010
comment
Пробовали ли вы переупорядочивать циклы short-byte-int, чтобы увидеть, есть ли разница? На тот случай, если JIT-компилятор решит, что третий цикл стоит оптимизировать, так как это обычная операция. Просто мысль. Было бы интересно посмотреть. - person Skizz; 08.04.2010
comment
@Aaronaught Переключение моей конфигурации на x86 dll выровняло мои результаты. Вот почему я предположил, что вы используете 32-битную операционную систему. - person Steve; 12.04.2010
comment
@Skizz Я исключил это заранее из-за нескольких прогонов не по порядку. Смотрите правки к моему сообщению - person Steve; 12.04.2010

Немного поздновато для игры, но этот вопрос заслуживает точного ответа.

Сгенерированный код IL для цикла int действительно будет быстрее, чем два других. При использовании byte или short требуется инструкция преобразования. Возможно, однако, что джиттер способен оптимизировать его при определенных условиях (не в рамках данного анализа).

Ориентир

Таргетинг на .NET Core 3.1 с Release (Any CPU) конфигурацией. Тест выполнен на x64 ЦП.


|    Method |      Mean |    Error |   StdDev |
|---------- |----------:|---------:|---------:|
|  ByteLoop | 149.78 ns | 0.963 ns | 0.901 ns |
| ShortLoop | 149.40 ns | 0.322 ns | 0.286 ns |
|   IntLoop |  79.38 ns | 0.764 ns | 0.638 ns |

Сгенерированный ИЛ

Сравнивая IL для трех методов, становится очевидным, что индуцированная стоимость возникает из-за инструкции conv.

IL_0000:  ldc.i4.0
IL_0001:  stloc.0
IL_0002:  br.s       IL_0009
IL_0004:  ldloc.0
IL_0005:  ldc.i4.1
IL_0006:  add
IL_0007:  conv.i2   ; conv.i2 for short, conv.i4 for byte
IL_0008:  stloc.0
IL_0009:  ldloc.0
IL_000a:  ldc.i4     0xff
IL_000f:  blt.s      IL_0004
IL_0011:  ret

Полный тестовый код

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace LoopPerformance
{
    public class Looper
    {
        [Benchmark]
        public void ByteLoop()
        {
            for (byte b = 0; b < 255; b++) {}
        }

        [Benchmark]
        public void ShortLoop()
        {
            for (short s = 0; s < 255; s++) {}
        }

        [Benchmark]
        public void IntLoop()
        {
            for (int i = 0; i < 255; i++) {}
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Looper>();
        }
    }
}
person l33t    schedule 05.10.2020

Профилирование кода .Net очень сложно, потому что среда выполнения, в которой выполняется скомпилированный байт-код, может выполнять оптимизацию байт-кода во время выполнения. Во втором примере компилятор JIT, вероятно, заметил повторяющийся код и создал более оптимизированную версию. Но без действительно подробного описания того, как работает система времени выполнения, невозможно узнать, что произойдет с вашим кодом. И было бы глупо пытаться угадать на основе экспериментов, поскольку Microsoft имеет полное право перепроектировать механизм JIT в любое время, при условии, что они не нарушают никаких функций.

person Skizz    schedule 07.04.2010
comment
Запуск кода в отладчике (или, точнее, компиляция и запуск с настройками по умолчанию для профиля отладки, с которым создается проект VS) практически исключает возможность оптимизации, о которой вы говорите. - person Adam Robinson; 07.04.2010
comment
@Adam: Но кто будет запускать код под отладчиком. Я заметил, что в VS2005 код выполняется намного медленнее в отладчике, чем в одиночку. IIRC, кто-то здесь упомянул, что выходные данные компилятора отладки .net и компилятора .net выпуска были почти идентичны, и именно тот факт, что код выполнялся автономно, а не в отладчике, имел значение. - person Skizz; 08.04.2010
comment
Отключение оптимизации (которое выполняется по умолчанию в конфигурации отладки) — это именно то, что устраняет оптимизацию, о которой вы говорите. Подключение любого отладчика может отрицательно сказаться на производительности, но это другой вопрос. Выходные данные компилятора с включенными оптимизациями действительно отличаются от выходных данных с отключенными оптимизациями. - person Adam Robinson; 08.04.2010

Консольная запись не имеет ничего общего с фактической производительностью данных. Это больше связано с взаимодействием с вызовами консольной библиотеки. Предложите вам сделать что-то интересное внутри этих циклов, не зависящее от размера данных.

Предложения: битовые сдвиги, умножения, манипуляции с массивами, сложение и многое другое...

person dawg    schedule 08.04.2010

Помимо производительности различных интегральных типов данных, я проверил производительность Int32 против Int64 (т. е. int против long) для реализации моего калькулятора простых чисел и обнаружил, что на моей машине x64 (Ryzen 1800X) заметной разницы не было.

Я не смог проверить с шортами (Int16 и UInt16), потому что они довольно быстро переполняются.

И, как отмечали другие, ваши короткие циклы запутывают ваши результаты и особенно ваши операторы отладки. Вместо этого вы должны попытаться использовать рабочий поток.


Вот сравнение производительности int и long:

введите здесь описание изображения

Конечно, избегайте long (и всего, кроме простого int) для индексов массива, поскольку вы даже не можете их использовать, а приведение к int может только снизить производительность (неизмеримо в моем тесте).

введите здесь описание изображения

Вот мой код профилирования, который опрашивает прогресс, поскольку рабочий поток вращается вечно. Он немного замедляется при повторных тестах, поэтому я обязательно протестировал в других порядках, а также по отдельности:

public static void Run() {
    TestWrapper(new PrimeEnumeratorInt32());
    TestWrapper(new PrimeEnumeratorInt64());
    TestWrapper(new PrimeEnumeratorInt64Indices());
}

private static void TestWrapper<X>(X enumeration)
where X : IDisposable, IEnumerator {
    int[] lapTimesMs = new int[] { 100, 300, 600, 1000, 3000, 5000, 10000 };
    int sleepNumberBlockWidth = 2 + (int)Math.Ceiling(Math.Log10(lapTimesMs.Max()));
    string resultStringFmt = string.Format("\tTotal time is {{0,-{0}}}ms, number of computed primes is {{1}}", sleepNumberBlockWidth);

    int totalSlept = 0;
    int offset = 0;
    Stopwatch stopwatch = new Stopwatch();

    Type t = enumeration.GetType();
    FieldInfo field = t.GetField("_known", BindingFlags.NonPublic | BindingFlags.Instance);

    Console.WriteLine("Testing {0}", t.Name);

    _continue = true;
    Thread thread = new Thread(InfiniteLooper);
    thread.Start(enumeration);
    stopwatch.Start();
    foreach (int sleepSize in lapTimesMs) {
        SleepExtensions.SleepWithProgress(sleepSize + offset);

        //avoid race condition calling the Current property by using reflection to get private data
        Console.WriteLine(resultStringFmt, stopwatch.ElapsedMilliseconds, ((IList)field.GetValue(enumeration)).Count);

        totalSlept += sleepSize;
        offset = totalSlept - (int)stopwatch.ElapsedMilliseconds;//synchronize to stopwatch laps
    }
    _continue = false;
    thread.Join(100);//plz stop in time (Thread.Abort is no longer supported)
    enumeration.Dispose();
    stopwatch.Stop();
}

private static bool _continue = true;
private static void InfiniteLooper(object data) {
    IEnumerator enumerator = (IEnumerator)data;
    while (_continue && enumerator.MoveNext()) { }
}

}

Обратите внимание, что вы можете заменить SleepExtensions.SleepWithProgress только на Thread.Sleep

И три варианта профилируемого алгоритма:

Версия Int32

class PrimeEnumeratorInt32 : IEnumerator<int> {
    public int Current { get { return this._known[this._currentIdx]; } }
    object IEnumerator.Current { get { return this.Current; } }

    private int _currentIdx = -1;
    private List<int> _known = new List<int>() { 2, 3 };

    public bool MoveNext() {
        if (++this._currentIdx >= this._known.Count)
            this._known.Add(this.ComputeNext(this._known[^1]));
        return true;//no end
    }

    private int ComputeNext(int lastKnown) {
        int current = lastKnown + 2;//start at 2 past last known value, which is guaranteed odd because we initialize up thru 3

        int testIdx;
        int sqrt;
        bool isComposite;
        while (true) {//keep going until a new prime is found
            testIdx = 1;//all test values are odd, so skip testing the first known prime (two)
            sqrt = (int)Math.Sqrt(current);//round down, and avoid casting due to the comparison type of the while loop condition

            isComposite = false;
            while (this._known[testIdx] <= sqrt) {
                if (current % this._known[testIdx++] == 0L) {
                    isComposite = true;
                    break;
                }
            }

            if (isComposite) {
                current += 2;
            } else {
                return current;//and end
            }
        }
    }

    public void Reset() {
        this._currentIdx = -1;
    }
    public void Dispose() {
        this._known = null;
    }
}

Версия Int64

class PrimeEnumeratorInt64 : IEnumerator<long> {
    public long Current { get { return this._known[this._currentIdx]; } }
    object IEnumerator.Current { get { return this.Current; } }

    private int _currentIdx = -1;
    private List<long> _known = new List<long>() { 2, 3 };

    public bool MoveNext() {
        if (++this._currentIdx >= this._known.Count)
            this._known.Add(this.ComputeNext(this._known[^1]));
        return true;//no end
    }

    private long ComputeNext(long lastKnown) {
        long current = lastKnown + 2;//start at 2 past last known value, which is guaranteed odd because we initialize up thru 3

        int testIdx;
        long sqrt;
        bool isComposite;
        while (true) {//keep going until a new prime is found
            testIdx = 1;//all test values are odd, so skip testing the first known prime (two)
            sqrt = (long)Math.Sqrt(current);//round down, and avoid casting due to the comparison type of the while loop condition

            isComposite = false;
            while (this._known[testIdx] <= sqrt) {
                if (current % this._known[testIdx++] == 0L) {
                    isComposite = true;
                    break;
                }
            }

            if (isComposite)
                current += 2;
            else
                return current;//and end
        }
    }

    public void Reset() {
        this._currentIdx = -1;
    }
    public void Dispose() {
        this._known = null;
    }
}

Int64 для значений и индексов

Обратите внимание на необходимое приведение индексов для доступа к списку _known.

class PrimeEnumeratorInt64Indices : IEnumerator<long> {
    public long Current { get { return this._known[(int)this._currentIdx]; } }
    object IEnumerator.Current { get { return this.Current; } }

    private long _currentIdx = -1;
    private List<long> _known = new List<long>() { 2, 3 };

    public bool MoveNext() {
        if (++this._currentIdx >= this._known.Count)
            this._known.Add(this.ComputeNext(this._known[^1]));
        return true;//no end
    }

    private long ComputeNext(long lastKnown) {
        long current = lastKnown + 2;//start at 2 past last known value, which is guaranteed odd because we initialize up thru 3

        long testIdx;
        long sqrt;
        bool isComposite;
        while (true) {//keep going until a new prime is found
            testIdx = 1;//all test values are odd, so skip testing the first known prime (two)
            sqrt = (long)Math.Sqrt(current);//round down, and avoid casting due to the comparison type of the while loop condition

            isComposite = false;
            while (this._known[(int)testIdx] <= sqrt) {
                if (current % this._known[(int)testIdx++] == 0L) {
                    isComposite = true;
                    break;
                }
            }

            if (isComposite)
                current += 2;
            else
                return current;//and end
        }
    }

    public void Reset() {
        this._currentIdx = -1;
    }
    public void Dispose() {
        this._known = null;
    }
}

Итого, моя тестовая программа использует 43 МБ памяти через 20 секунд для Int32 и 75 МБ памяти для Int64 из-за коллекции List<...> _known, что является самой большой разницей, которую я наблюдаю.


Я также профилировал версии, используя неподписанные типы. Вот мои результаты (режим выпуска):

Testing PrimeEnumeratorInt32
        Total time is 20000 ms, number of computed primes is 3842603
Testing PrimeEnumeratorUInt32
        Total time is 20001 ms, number of computed primes is 3841554
Testing PrimeEnumeratorInt64
        Total time is 20001 ms, number of computed primes is 3839953
Testing PrimeEnumeratorUInt64
        Total time is 20002 ms, number of computed primes is 3837199

Все 4 версии имеют практически одинаковую производительность. Я полагаю, что урок здесь состоит в том, чтобы никогда не предполагать, как это повлияет на производительность, и что вам следует вероятно использовать Int64, если вы ориентируетесь на архитектуру x64, поскольку она соответствует моей версии Int32 даже при увеличенном использовании памяти.

И проверка, что мой основной калькулятор работает:

введите здесь описание изображения

P.S. В режиме выпуска были стабильные результаты, которые были на 1,1% быстрее.

П.П.С. Вот необходимые операторы using:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
person Elaskanator    schedule 02.10.2020