C#. Guid.NewGuid(). Linux. Windows. Randomness or Uniqueness. RNG and PRNG. Performance. Benchmarking.

Цель нашей сегодняшней сказки — развлечься как следует. Детективная история в поисках потерянного перфоманса с красивым финалом и эффектным результатом непосредственно связана с набором слов из предыдущего абзаца.

Художественное отступление, шутка на текущую тему. Читать совершенно не обязательно.

— О, Tux, давно не виделись! — Con, широко улыбаясь, выглядывал из распахнутого окна.

— Здравствуй, Con! Вот, вернулся ненадолго. Как дела, как жизнь?

— Да всё также. За жильё приходится платить всё больше и больше. Пугают всё время, что что-нибудь отключат или запретят. Но пока хорошо, красиво, удобно, привычно. Да и от старых вещей избавляться не хочется, мороки много с этим. Расскажи лучше, как твой переезд, каково там?

— Знаешь, Con, я толком понять не успел. Жильё бесплатное. Но обустраивать самому приходится. В целом, всё также. И одновременно всё не так… точно могу сказать, что переезжать было не просто. Тосковал по старому дому, привыкал к новому климату. От некоторых старых вещей пришлось избавляться, потому что нельзя с ними переезжать. Ещё, точно могу сказать, что жить там тяжелее. Как будто на само существование нужно тратить больше сил, больше внутренних ресурсов.

Tux на секунду задумался. Но быстро спохватился и увлеченно продолжил говорить.

— А недавно я заметил вообще интересную вещь! Я стал очень много времени проводить в придумывании уникальных имен всяким своим штукам. Не знаю почему. Раньше раз, и выдумывалось быстро. А сейчас, как только пытаюсь дать уникальный идентификатор чему-нибудь, так сразу зависаю надолго.

— Во дела.. а чего ты именно туда-то поехал, а, Tux?

— Да просто. Пингвинов хотел посмотреть, очень их люблю.

Каждый раз, когда мы хотим выдумать уникальный идентификатор для какой-нибудь сущности, чтобы она уж точно не была ни на что не похожей, первым в голову приходит Guid. Или, если назвать его имя полностью, Globally Unique Identifier.

Guid — это конкретная реализация стандарта UUID (Universally Unique Identifier).

Guid — это «случайные» 128 бит. Или, 2^128 вариантов (на самом деле чуточку меньше, там есть зарезервированные биты). Это ОЧЕНЬ много. Вероятность сгенерировать два одинаковых Guid'а невероятно мала. Настолько мала, что все условились считать их уникальными.

Guid — это не про security, это про uniqueness. То есть его не стоит использовать для криптографических целей или даже для генерации паролей. Хотя, нам повезло и на Windows (и даже на Linux) по факту прямо сейчас нам дают 122 бит энтропии. И вроде даже обещают не ломать это в будущем (в том же XML документе).

Guid — весьма интересная штука. И продолжать накидывать про неё фактов можно очень долго.

Дело было вечером, делать было нечего

Как-то раз ковырялись в трейсах приложений в поисках ответа на вопрос «почему один и тот же код на Линуксе потребляет в полтора раза больше CPU, чем на Винде». Помимо прочего, в глаза бросалась большая разница во времени, проведённом приложением внутри функции Guid.NewGuid(). На Линуксе оно занимало аж 5.4% от всего времени работы приложения. А на Винде сильно меньше. Вот скриншот PerfView, на котором видно функцию генерации гуида:

Первые две строчки на скриншоте выше — это бессмысленные ожидающие потоки. Они практически ничего не потребляют у приложения, но занимают много времени, отчего всплыли в топ. Если их скрыть, то выйдет, что генерация гуидов занимает аж 8.6% от времени работы приложения (На Windows эта функция потребляла намного меньше):

Почему употребляется термин время, а не CPU?

Средства анализа .NET приложений показывают несколько разную картину в зависимости от ОС, на которой было запущено само приложение. Так, большинство CPU работы, проведённой внутри ОС Linux, неотличимы от всяческих ожиданий при изучении приложения с помощью PerfView. Например, Thread.Sleep() будет неотличим от CPU-bound функции в .nettrace артефактах.

Такие трейсы, снятые на Linux, померили нам время, проведённое всеми тредами приложения в этих стеках. Вне зависимости от того, работал ли CPU внутри этого стека. И в данный момент они попадают в категорию «UNMANAGED_CODE_TIME». В «CPU_TIME» находится только наш managed C# код.

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

Изучаем Guid.NewGuid()

Не то чтобы медленный Guid.NewGuid() был основной причиной нашей проблемы. Было очевидно, что разница в нескольких процентах от всего приложения — это не то, что мы ищем, расследуя разницу в полтора раза. Но всё-таки было интересно, что такого особенного в генерации гуида и откуда такая разница в зависимости от ОС.

Чтобы это выяснить, мы соорудили предельно простой бенчмарк одной функции: Guid.NewGuid(). Запустили на Windows и на Linux (Centos 7) на виртуалках одинаковой конфигурации.

Это важно!

Запускать в каком-нибудь WSL было бы неправильно. Это всё-таки эмулятор (на самом деле далеко нет, но такому бенчмарку доверять всё равно было бы нельзя). А процессоры машин разработчиков это не процессоры на проде.

Результат подтвердил то, что мы видели в трейсах:

Linux:
|              Method |        Mean | Allocated |
|-------------------- |------------:|----------:|
|         GuidNewGuid | 1,293.91 ns |         - |

Windows:
|              Method |     Mean | Allocated |
|-------------------- |---------:|----------:|
|         GuidNewGuid | 80.71 ns |         - |

// Колонка Allocated с прочерком оставлена намерено.
// Это значит, что при генеарции Гуида ничего не аллоцируется на хипе.
// Что прекрасно.
Извините, скриншоты потерялись, остались лишь текстовые представления

Но легко повторить такой эксперимент самостоятельно. Дам лишь небольшой хинт. Если вы захотите считерить и собрать проект с бенчмарком у себя на рабочей машине, чтобы затем перенести лишь исполняемые файлы для запуска на другой виртуалке, разочарую вас. Нужно именно собрать проект на целевой машине и запускать бенчмарк из папки с проектом. Этому есть причины. BenchmarkDotNet умный, но хитрый. И хитрый, но умный ;)

Разница просто колоссальная. Создание Guid'а на Линуксе в 16 раз медленнее, чем на Винде! Давайте разбираться, почему так.

Разбираемся

Хочется заглянуть внутрь реализации и посмотреть, что же там такого интересного. Раз производительность зависит от ОС, доверять декомпиляции метода Guid.NewGuid() на своей рабочей машине неправильно. Отправляемся на гитхаб, где можно найти исходники. Находим там два файла с кусочками partial class'ов: Guid.Unix.cs и Guid.Windows.cs.

Кстати, сразу видим интересный комментарий, что генерация Guid'а на Linux'е намеренно пользуется криптографически стойким генератором случайных чисел. Как и в Windows. Но генерация гуида это всё ещё не рекомендуемый способ придумывать «действительно» криптостойкие ключи. Как минимум потому, что в нём есть один предсказуемый бит версии, а утечка хотя бы одного бита информации может быть фатальной.

Linux

Если раскапывать цепочку системных вызовов в случае Linux ветки, можно докопаться до вот такого кода на C. Если кратко, то мы читаем случайные байты из /dev/urandom. Несмотря на то, что в man /dev/urandom говорится, что его не стоит использовать в криптографических целях, это не так. Исключения составляют, кажется, первые мгновения жизни Linux системы, когда генератор шумов ещё не набрал нужный уровень «случайности» (я не умею это доказывать и не имею ссылки на авторитетный источник).

Информация актуальна на момент написания статьи.

Если покопаться в истории коммитов, то там было множество различных вариантов реализации. Например, какое-то время случайные числа брались из /dev/random. Это генератор «действительно» случайных чисел из шумов процессора (RNG), однопоточный и блокирующий вызовы до тех пор, пока не накопится достаточно «случайности».

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

Windows

Если кратко говорить про Windows, то тут мы пользуемся функцией CoCreateGuid, которая просто перевызывает RPC функцию UuidCreate. А она, в свою очередь, перевызывает генератор случайных чисел CryptGenRandom (или RtlGenRandom в каких-то очень старых версиях). Дальше и подробнее что-то рассказать сложно, потому что, кажется, те исходники, что я нашел в интернете, получены не самым честным путём.

Дак что в итоге?

Судя по ответам на гитхабе, цепочка вызовов на Windows просто быстрее, чем рассмотренное нами выше чтение из /dev/urandom на Linux, поскольку несет в себе меньше оверхеда на всякие системные штуки. Печально.

Как можно обойти эту нелепую ситуацию?

Давайте вернемся в самое начало, к трейсу. На самом деле, если поизучать его более пристально, то видно, что 100% всех гуидов в нашем приложении мы генерируем для телеметрии:

Vostok.Tracing - библиотека, имя которой говорит за себя. Она создаёт трейсы, а метод BeginSpan создаёт спаны. Аналогичные тем, что можно увидеть, например, в opentelementry. Vostok.Hercules.Client - библиотека, вспомогательная к сбору этой телеметрии.

В телеметрии нам уж точно не нужна криптостойкость. Нам просто хочется, чтобы гуиды редко совпадали. И кажется, что можно попробовать взять быстрый и простой псевдослучайный генератор случайных чисел (PRNG) и с помощью него создавать гуиды. В C# уже есть Random, генератор псевдослучайных чисел. Давайте его и попробуем.

Что такое гуид? 16 байт. Random предназначен для того, чтобы генерировать int, то есть 4 байта. Давайте вызовем Random.Next() 4 раза и соберем из них Guid. Даже не будем выставлять нужные битики, как того требует RFC, кому они нужны. И ещё, да, мы потеряем целых 4 бита на знаковых битах int'а, поскольку Random генерирует только положительные числа. Из 128 бит осталось 124 псевдослучайных. Этого уж точно хватит на нашу телеметрию.

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

Реализация и снова бенчмарки

internal static class ThreadSafeRandom
{
    [ThreadStatic]
    private static Random random;
    
    public static int Next()
    {
        return ObtainRandom().Next();
    }

    private static Random ObtainRandom()
    {
        return random ?? (random = new Random(Guid.NewGuid().GetHashCode()));
    }
}

internal static class GuidGenerator
{
    public static unsafe Guid GuidRandom()
    {
        var bytes = stackalloc byte[16];
        var dst = bytes;
        for (var i = 0; i < 4; i++)
        {
            *(int*)dst = ThreadSafeRandom.Next();
            dst += 4;
        }

        return *(Guid*)bytes;
    }
}

Снова запускаем наш бенчмарк, смотрим, что получилось:

Linux:
|              Method |        Mean | Allocated |
|-------------------- |------------:|----------:|
|         GuidNewGuid | 1,293.91 ns |         - |
|          GuidRandom |   108.66 ns |         - |

Windows:
|              Method |     Mean | Allocated |
|-------------------- |---------:|----------:|
|         GuidNewGuid | 80.71 ns |         - |
|          GuidRandom | 79.96 ns |         - |

Отлично! Теперь мы умеем генерировать «Guid» на Linux почти так же быстро, как на Windows! Смущает разве что следующее. Используя метод Guid.NewGuid() на Windows, мы обращаемся в winapi, используем криптостойкий генератор случайных чисел, и вообще делаем interop за пределы .NET'а. А наш метод — чистый managed код. И работает с такой же позорной скоростью.

Нетрудно догадаться, на что потратилось много времени. Чтобы сгенерировать 16 байт нам нужно 4 раза вызвать Next(). А значит 4 раза обратиться к ThreadStatic полю. И доступ к ThreadStatic полю, очевидно, дорогой. Воспользуемся некоторыми знаниями устройства тредпула и работы потоков, чтобы обойти это.

Мы делаем 4 обращения к полю random в цикле. Это целиком и полностью синхронный код. Значит между итерациями цикла не может быть context switch'ей (на уровне .NET'а). А значит ThreadID всегда будет одинаковый. А значит мы все 4 раза будем доставать за дорого один и тот же инстанс класса Random. Дак давайте достанем его один раз.

internal static class ThreadSafeRandom
{
    [ThreadStatic]
    private static Random random;
    
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Random ObtainThreadStaticRandom() => ObtainRandom();

    private static Random ObtainRandom()
    {
        return random ?? (random = new Random(Guid.NewGuid().GetHashCode()));
    }
}

internal static class GuidGenerator
{
    public static unsafe Guid GuidRandomCachedInstance()
    {
        var bytes = stackalloc byte[16];
        var dst = bytes;
      
        var random = ThreadSafeRandom.ObtainThreadStaticRandom();
        for (var i = 0; i < 4; i++)
        {
            *(int*)dst = random.Next();
            dst += 4;
        }
    
        return *(Guid*)bytes;
    }
}

Снова запускаем наш бенчмарк. Для сравнения запустим и старую и новую версию нашего собственного генератора Guid'ов.

Linux:
|                   Method |        Mean | Allocated |
|------------------------- |------------:|----------:|
|              GuidNewGuid | 1,293.91 ns |         - |
|               GuidRandom |   108.66 ns |         - |
| GuidRandomCachedInstance |    56.45 ns |         - |

Windows:
|                   Method |     Mean | Allocated |
|------------------------- |---------:|----------:|
|              GuidNewGuid | 80.71 ns |         - |
|               GuidRandom | 79.96 ns |         - |
| GuidRandomCachedInstance | 43.73 ns |         - |

Чудесно. Мы умеем генерировать не-криптостойкий «Guid» за 31 строчку кода. Который на Windows быстрее встроенного Guid.NewGuid() в ~2 раза. А на Linux быстрее встроенного Guid.NewGuid() в ~23 раза!

А можно ещё лучше?

Предложенная выше реализация справедлива для Netstandard2.0. Всё-таки, поддерживать старые проекты под .NET FW нужно, не все из них обновлены до современных версий .NET. Но, к счастью, мы можем писать код так, чтобы под свежей версией .NET использовался другой код. А в .NET 6 как раз появилось несколько полезных вещей.

Воспользуемся сразу двумя фичами .NET 6. Во-первых, появился специализированный Random.Shared. Больше не нужно извращаться с атрибутом [ThreadStatic]. Теперь код нашего ThreadSafeRandom можно написать так (или, при желании, вообще от него избавиться):

internal static class ThreadSafeRandom
{
#if NET6_0_OR_GREATER
#else
    [ThreadStatic]
    private static Random random;
#endif

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Random ObtainThreadStaticRandom() => ObtainRandom();

    private static Random ObtainRandom()
    {
#if NET6_0_OR_GREATER
            return Random.Shared;
#else
            return random ?? (random = new Random(Guid.NewGuid().GetHashCode()));
#endif
    }
}

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

В .NET 6 есть способы генерировать куда больше случайных байт за один вызов, чем 4. А именно, появился NextInt64(). А в паре с ним появился и NextBytes(Span<byte> buffer). Незамедлительно ими воспользуемся. Приведу пример только с NextBytes:

internal static class GuidGenerator
{
    public static unsafe Guid GenerateNotCryptoQualityGuid()
    {
        var bytes = stackalloc byte[16];
        var dst = bytes;

        var random = ThreadSafeRandom.ObtainThreadStaticRandom();

#if NET6_0_OR_GREATER
        random.NextBytes(new Span<byte>(bytes, 16));
#else
        for (var i = 0; i < 4; i++)
        {
            *(int*)dst = random.Next();
            dst += 4;
        }
#endif

            return *(Guid*)bytes;
        }
    }
}

Давайте проверим, стало ли лучше? Снова запустим бенчмарк.

Linux:
|        Method |            Runtime |        Mean |
|-------------- |------------------- |------------:|
|   GuidNewGuid |           .NET 6.0 | 1,274.87 ns |
| GenerateInt32 |           .NET 6.0 |    55.88 ns |
| GenerateInt64 |           .NET 6.0 |    28.97 ns |
| GenerateBytes |           .NET 6.0 |    18.85 ns |

Windows:
|        Method |            Runtime |     Mean |
|-------------- |------------------- |---------:|
|   GuidNewGuid | .NET Framework 4.8 | 81.36 ns |
| GenerateInt32 | .NET Framework 4.8 | 56.41 ns |
|-------------- |------------------- |---------:|
|   GuidNewGuid |           .NET 5.0 | 80.87 ns |
| GenerateInt32 |           .NET 5.0 | 45.13 ns |
|-------------- |------------------- |---------:|
|   GuidNewGuid |           .NET 6.0 | 80.38 ns |
| GenerateInt32 |           .NET 6.0 | 46.70 ns |
| GenerateInt64 |           .NET 6.0 | 23.35 ns |
| GenerateBytes |           .NET 6.0 | 16.76 ns |

Я не стал приводить весь код бенчмарка, чтобы не нагромождать статью однотипным кодом. Поэтому просто дам краткую расшифровку:

  • GuidNewGuid — генерация вызовом метода Guid.NewGuid().

  • GenerateInt32 — генерация вызовом метода random.Next() 4 раза.

  • GenerateInt64 — генерация вызовом метода random.NextInt64() 2 раза.

  • GenerateBytes — генерация вызовом метода random.NextBytes(Span<byte> buffer).

Чудесно. Мы умеем генерировать не-криптостойкий «Guid», который на Windows быстрее встроенного Guid.NewGuid() в ~5 раз. А на Linux быстрее встроенного Guid.NewGuid() в ~67 раз!

Извлечём практическую пользу

Не пропадать же добру.

Этот генератор Guid'ов уже давно крутится в куче сервисов. С помощью него мы генериуем идентификаторы спанов, трейсов, и прочих эвентов телеметрии. Код можно найти на гитхабе: GuidGenerator и одно из использований, генерация трейсов и спанов. И вся данная телеметрия успешно отправляется и обрабатывается в Hercules’е — системе сбора, хранения и обработки логов, метрик, распределённых трассировок и аннотаций.

Но тут не всё рассказано!

Как обычно, мы прошли мимо большого количества всяких интересностей:

  • А можем ли мы генерировать псевдослучайные числа ещё быстрее? Что на счет максимально быстрой реализации на SIMD? Конечно можем. Всех интересующихся направляю на SIMDxorshift. Здесь не буду утомлять кодом на C и SIMD-инструкциями.

  • А есть что-нибудь ещё в .NET'е, что неожиданно и кардинально меняет свою производительность при переходе с Windows на Linux? Конечно есть. И много чего. Да хотя бы работа со строками и кодировками чего только стоит. Но об этом, может быть, в другой раз.

  • Что за заклинания с [MethodImpl(MethodImplOptions.AggressiveInlining)] и (int*)? Достойно отдельной статьи. Двух разных.

  • Как снимать и как анализировать трейсы .NET приложений? Тоже достойно отдельной статьи.

  • Почему на виртуалках одинаковой конфигурации наш полностью managed-код с использованием Random все равно имеет разную скорость исполнения на Linux и Windows? Сложно сказать. Разница небольшая и, возможно, виртуалки просто крутились на разных гипервизорах с разной производительностью железа.

Если углубляться в каждый нюанс, статья получится бесконечной. Но вы всегда можете предложить какую-нибудь конкретную тему для следующей статьи в комментариях или в личке.

Комментарии (26)


  1. trokhymchuk
    18.04.2022 13:40
    +1

    Судя по ответам на гитхабе, цепочка вызовов на Windows просто быстрее, чем рассмотренное нами выше чтение из /dev/urandom на Linux

    Не так давно random был переработан, интересно, насколько сильно это отразится на сабже.


  1. Alew
    18.04.2022 14:06
    +1

    А почему TreadStatic дорогой?


    1. deniaa Автор
      18.04.2022 15:15
      +6

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

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

      В общем, там внутри вполне понятный код, который надо исполнить. И он дорог относительно той работы, что мы бенчмаркали, чтобы стало осмысленно его минимизировать. Я бы порекомендовал ознакомиться с устройством этого атрибута в исходниках на гитхабе, забенчмаркать доступ к TreadStatic полям. Это будет увлекательно!


      1. majiq
        20.04.2022 08:45

        Ага, 'приключение на 20 минут' :)


    1. Nagg
      18.04.2022 16:36
      +5

      в теории он должен быть супер дешевым на большинстве платформе где есть спец регистры указывающие на текущий поток, просто пока это не сделано в дотнете - я файлил ишью https://github.com/dotnet/runtime/issues/63619


  1. aamonster
    18.04.2022 14:08
    +2

    Выглядит так, будто для ваших нужд вообще достаточно получить одно случайное (или даже не случайное) число, а дальше крутить свой генератор (вихрь Мерсена или ещё что) прямо в юзерспейсе. Хотя вряд ли есть необходимость настолько упираться в оптимизацию...


    1. vesper-bot
      18.04.2022 14:58

      Мне интересно, насколько криптостоек /dev/urandom, чтобы имело смысл даже в обычной реализации NewGuid() для линукса "крутить Мерсенна" вместо лазанья в системное устройство в целях повышения производительности. Вроде как предсказывать следующий GUID в таких задачах банально незачем.
      А ещё, вроде как /dev/urandom довольно медленный источник байт, помнится, когда я попытался сделать dd if=/dev/urandom ..., у меня производительность была около 10 МБ/с всего лишь. Я не удивлюсь, если и в случае ситуации автора они банально уперлись в своем бенчмарке в скорость генерации случайных чисел устройством, вместо того, чтобы нарваться на излишний системный оверхэд. Один гуид 16 байт, получаем около 1.0 мкс ~= 16 МБ/с, немногим быстрее моего значения (и я мог приврать до одного двоичного порядка вверх, т.е. 10-20 МБ/с, помню, что 1х было в скорости).


      1. edo1h
        18.04.2022 19:37

        помнится, когда я попытался сделать dd if=/dev/urandom ..., у меня производительность была около 10 МБ/с всего лишь

        с тех пор его достаточно неплохо ускорили, я на своей машине сейчас вижу ≈235 МБ/с на 512-байтных блоках и ≈295 МБ/с на мегабайтных.


    1. deniaa Автор
      18.04.2022 16:08
      +1

      Действительно, если поставить себе задачу "максимально эффективно генерировать уникальные идентификаторы в распределённом кластере", то наверняка нужно было бы идти каким-то таким путём, как вы предложили.

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


  1. lightmg
    18.04.2022 15:04
    +2

    Есть ли причины, почему в итоговом примере со Span<byte>() используется unsafe-код, а не перегрузка конструктора, принимающая Span?


    1. deniaa Автор
      18.04.2022 15:31
      +3

      Никаких причин нет. Такой (действительно, менее красивый) код остался от старого варианта, когда такого конструктора ещё не существовало.

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


  1. kemsky
    18.04.2022 19:37

    Была ещё библиотека от Додо, думаю вам будет интересно посмотреть https://github.com/dodopizza/primitives


    1. beskaravaev
      19.04.2022 10:40

      Библиотечка очень интересная, но внутри генератора всё вызывается Guid.NewGuid() от которого они хотели избавиться, так что она не подойдёт.

      Spoiler header
      public static Uuid NewTimeBased()
              {
                  byte* resultPtr = stackalloc byte[16];
                  var resultAsGuidPtr = (Guid*) resultPtr;
                  var guid = Guid.NewGuid();
                  resultAsGuidPtr[0] = guid;
                  long currentTicks = DateTime.UtcNow.Ticks - ChristianCalendarGregorianReformTicksDate;
                  var ticksPtr = (byte*) &currentTicks;
                  resultPtr[0] = ticksPtr[3];
                  resultPtr[1] = ticksPtr[2];
                  resultPtr[2] = ticksPtr[1];
                  resultPtr[3] = ticksPtr[0];
                  resultPtr[4] = ticksPtr[5];
                  resultPtr[5] = ticksPtr[4];
                  resultPtr[6] = (byte) ((ticksPtr[7] & ResetVersionMask) | Version1Flag);
                  resultPtr[7] = ticksPtr[6];
                  resultPtr[8] = (byte) ((resultPtr[8] & ResetReservedMask) | ReservedFlag);
                  return new Uuid(resultPtr);
              }
      


      p.s. Да и решают авторы статьи и библиотеки разные задачи


  1. edo1h
    18.04.2022 19:39

    Даже не будем выставлять нужные битики, как того требует RFC, кому они нужны

    ради экономии нескольких строчек кода? мне не нравится )


  1. mayorovp
    18.04.2022 20:16
    +1

    Если вам в принципе не нужна безопасность или случайность, а лишь уникальность — то зачем вообще все эти игры с Random?


    Держите вариант, которому вообще не требуется обращение к ГСЧ кроме как при инициализации:


        static class SequentialGuidGenerator
        {
            class ThreadState
            {
                private readonly byte[] buffer;
                private ulong next;
    
                public ThreadState(Guid seed)
                {
                    buffer = seed.ToByteArray();
                    next = BitConverter.ToUInt64(buffer, 8);
                }
    
                public Guid NewGuid()
                {
                    var span = buffer.AsSpan();
                    var r = BitConverter.TryWriteBytes(span[8..], unchecked(next++));
                    return new Guid(span);
                }
            }
    
            static readonly ThreadLocal<ThreadState> state = new(() => new(Guid.NewGuid()));
    
            public static Guid NewGuid() => state.Value.NewGuid();
        }


    1. deniaa Автор
      18.04.2022 22:10

      Если я всё сделал правильно, то мой вариант оказался чуть-чуть эффективнее ;)

      |                          Method |     Mean | Allocated |
      |-------------------------------- |---------:|----------:|
      |    GenerateNotCryptoQualityGuid | 17.84 ns |         - |
      | SequentialGuidGenerator.NewGuid | 21.36 ns |         - |

      Но и ваш вариант очень хорош и действительно не обращается к (P)RNG.


  1. wantsomeknowledge
    18.04.2022 21:41

    А можно пожалуйста для тупых пояснить?
    Есть строчка кода:

     return random ?? (random = new Random(Guid.NewGuid().GetHashCode()));

    Тут вызывается Guid.NewGuid() и берется от него HashCode. Вопрос: почему в этом случае нет тормозов, ведь мы вызываем все тот же метод, из-за которого и начался весь сыр-бор?


    1. deniaa Автор
      18.04.2022 21:45

      Потому, что new Random(Guid.NewGuid().GetHashCode()) на самом деле исполнится всего один раз, в момент первого обращения. В дальнейшем оператор ?? будет возвращать сконструированный инстанс random. Детали оператора ?? лучше посмотреть в документации.


      1. wantsomeknowledge
        18.04.2022 22:13

        что-то тупанул, спасибо)


  1. socketpair
    19.04.2022 00:39

    https://man7.org/linux/man-pages/man2/getrandom.2.html же. И не надо лохматить бабушку. А если у вас старинный центос в котором нет данного сисколла - то ссзб.


  1. Tinkturianec
    19.04.2022 02:11
    +2

    Мне кажется, или инициализация рандома

    return random ?? (random = new Random(Guid.NewGuid().GetHashCode()));

    плохо соотносится с мыслью
    Нам просто хочется, чтобы гуиды редко совпадали.

    Сид в этом случае составляет 4 байта, и порядка 100k попыток создать генератор хватит, чтобы скорее всего хотя бы у двух генераторов совпали сиды. После этого они начнут генерировать одинаковые последовательности случайных байтов и одинаковые последовательности гуидов.

    И если в одном процессе иметь 100k тредов может быть достаточно странно, то размазать их по сотням-тысячам процессов выглядит не очень сложной задачей.


    1. deniaa Автор
      19.04.2022 08:50

      Всё так, "парадокс дней рождений" :)

      К счастью, .NET 6 становится всё более распространённым (у нас), где используется Random.Shared. А в нём сид уже достаточно большой.


      1. Tinkturianec
        20.04.2022 18:07
        +1

        Может стоит выдать тем, кто ещё не на .net 6, и не может на него быстро перейти, какой-то другой рандом? Кажется, что такая схема генерации traceId может попортить много крови при попытке разбора конкретных ситуаций. Или сделать телеметрию практически бесполезной.


  1. socketpair
    19.04.2022 02:11

    https://man7.org/linux/man-pages/man2/getrandom.2.html же. И не надо лохматить бабушку. А если у вас старинный центос в котором нет данного сисколла - то ссзб.


  1. Visier
    19.04.2022 17:02

    Еще можно один раз сгенерировать Гуид, а потом его просто инкрементить.


  1. 3263927
    20.04.2022 04:01

    я для таких задач использую псевдо-guid, потому что туда можно ещё полезную информацию засунуть, например дату время + какое-то случайное число чтобы исключить совпадение если вдруг в одно и то же время произойдёт событие, плюс какие-то IDшники можно туда засунуть, тогда и уникальный будет и в то же время не просто балласт, а там будут какие-то данные, да и сортировать удобно - получается как постоянно увеличивающийся ключ