Pull to refresh

Comments 21

UFO just landed and posted this here
Если Base64, то самый простой способ это класс ToBase64Transform и CryptoStream.
using System.Security.Cryptography;

var base64Transform = new ToBase64Transform();
using var output = new MemoryStream();
using var base64Encoder = new CryptoStream(output, base64Transform, CryptoStreamMode.Write);
using var gZipCompressor = new GZipStream(base64Encoder, CompressionMode.Compress);

gZipCompressor.Write(new byte[] { 255}, 0, 1);


Если другие, то для стримов удобнее SimpleBase и класс StreamHelper.

Для чтения из сокетов и System.IO.Pipelines уже удобнее можно использовать мою либу т.к. там есть подходящий BaseNEncoder.Convert() принимающие Span{byte}.

Хотя если реализовать ICryptoTransform то можно сесть на паравоз CryptoStream и получить поддержку стримов. Я скорее всего скоро это сделаю у себя в либе в ближайшее время.
UFO just landed and posted this here
Типа такого?

public static Stream BaseNEncode(this Stream stream, BaseNAlphabet baseNAlphabet)
{
  return new CryptoStream(stream, new BaseNEncoder(baseNAlphabet), CryptoStreamMode.Write);
}

var output = new MemoryStream();
var writeStream = output
  .BaseNEncode(BaseNAlphabet.Base64Alphabet)
  .GZip();

writeStream.Write(...);


Естественно расширение GZip() я не могу поместить в свою библиотеку т.к. это вне зоны ее ответственности.
Добавил наследование ICryptoTransform пару расширений:
stream.BaseNEncode() // в полученный стрим можно писать
stream.BaseNDecode() // из полученного стрима можно читать


Тест с примером.

Ещё бы производительность всего этого дела сравнить. Например, в System.Buffers.Text.Base64 используются AVX2/SSE интринсики, а это должно давать нехилый выигрыш в производительности.


Я вот взял код отсюда:
https://github.com/deniszykov/BaseN/tree/master/src/deniszykov.BaseN.Benchmark
добавил бенчмарк, использующий System.Buffers.Text.Base64, и получил следующее (NET 5.0):


|                          Method |       Mean |     Error |    StdDev |     Median | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------------------------- |-----------:|----------:|----------:|-----------:|------:|------:|------:|------:|----------:|
|    Base64_Encode_ByteArray_Char | 109.008 ms | 0.1626 ms | 0.1442 ms | 108.978 ms |  1.00 |     - |     - |     - |   8.12 KB |
|    Base64_Encode_ByteArray_Byte | 111.295 ms | 0.4776 ms | 0.4234 ms | 111.190 ms |  1.02 |     - |     - |     - |   4.12 KB |
| SysBase64_Encode_ByteArray_Byte |   3.349 ms | 0.0010 ms | 0.0008 ms |   3.349 ms |  0.03 |     - |     - |     - |   4.06 KB |
|          Base64_Encode_Ptr_Char | 108.706 ms | 0.6456 ms | 0.5723 ms | 108.542 ms |  1.00 |     - |     - |     - |   8.12 KB |
|          Base64_Encode_Ptr_Byte | 103.263 ms | 0.2176 ms | 0.2036 ms | 103.219 ms |  0.95 |     - |     - |     - |   5.86 KB |
|         Base64_Encode_Span_Char | 103.621 ms | 0.0388 ms | 0.0363 ms | 103.622 ms |  0.95 |     - |     - |     - |   8.12 KB |
|         Base64_Encode_Span_Byte | 103.901 ms | 0.8819 ms | 0.8249 ms | 104.639 ms |  0.95 |     - |     - |     - |   4.12 KB |

Разница в 30 раз!
И ещё это при том, что на моём процессоре нет AVX2.

Вы использовали EncodeToUtf8 или EncodeToUtf8InPlace? Можете показать код этого теста?

[Benchmark]
public void SysBase64_Encode_ByteArray_Byte()
{
    var encoder = new BaseNDecoder(BaseNAlphabet.Base64Alphabet);
    var output = new byte[4 * 1024];
    for (var i = 0; i < data.Length;)
    {
        Base64.EncodeToUtf8(new ReadOnlySpan<byte>(data, i, data.Length - i), output, out var bytesConsumed, out var bytesWritten);
        i += bytesConsumed;
    }
}

Также полезно сравнить производительность в разрезе кодирования и декодирования утилитарными методами.


Encode (gist)


|                                 Method |      Mean |    Error |   StdDev | Ratio | RatioSD |
|--------------------------------------- |----------:|---------:|---------:|------:|--------:|
|          System_Convert_ToBase64String |  51.32 ms | 0.549 ms | 0.514 ms |  1.00 |    0.00 |
|           BaseN_Base64Convert_ToString | 164.16 ms | 1.372 ms | 1.283 ms |  3.20 |    0.04 |
|           BaseN_Base32Convert_ToString | 185.88 ms | 1.935 ms | 1.810 ms |  3.62 |    0.04 |
| Wiry_Base32Encoding_Standard_GetString | 108.34 ms | 1.035 ms | 0.968 ms |  2.11 |    0.03 |
|       SimpleBase_Base32_Rfc4648_Encode | 131.39 ms | 1.979 ms | 1.851 ms |  2.56 |    0.05 |
|                  Albireo_Base32_Encode | 184.59 ms | 1.801 ms | 1.685 ms |  3.60 |    0.05 |

Decode (gist)


|                               Method |        Mean |     Error |   StdDev | Ratio | RatioSD |
|------------------------------------- |------------:|----------:|---------:|------:|--------:|
|      System_Convert_FromBase64String |    86.81 ms |  0.619 ms | 0.579 ms |  1.00 |    0.00 |
|          BaseN_Base64Convert_ToBytes |   180.61 ms |  1.902 ms | 1.588 ms |  2.08 |    0.02 |
|          BaseN_Base32Convert_ToBytes |   200.32 ms |  1.687 ms | 1.408 ms |  2.30 |    0.02 |
| Wiry_Base32Encoding_Standard_ToBytes |    56.67 ms |  0.510 ms | 0.477 ms |  0.65 |    0.01 |
|     SimpleBase_Base32_Rfc4648_Decode |   104.86 ms |  1.144 ms | 1.070 ms |  1.21 |    0.02 |
|                Albireo_Base32_Decode | 1,485.12 ms | 10.363 ms | 9.694 ms | 17.11 |    0.17 |

Полный лог: gist

Добавьте System.Buffers.Text.Base64.EncodeToUtf8 — удивитесь.

О, да.


Код.


Intel Core i5-7440HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=5.0.101
  [Host]     : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
  DefaultJob : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT

|                                 Method |       Mean |     Error |    StdDev | Ratio | RatioSD |
|--------------------------------------- |-----------:|----------:|----------:|------:|--------:|
|          System_Convert_ToBase64String |  51.039 ms | 0.4572 ms | 0.4276 ms |  1.00 |    0.00 |
|           BaseN_Base64Convert_ToString | 162.071 ms | 0.9066 ms | 0.7571 ms |  3.17 |    0.03 |
|             System_Base64_EncodeToUtf8 |   2.416 ms | 0.0266 ms | 0.0236 ms |  0.05 |    0.00 |
|                              NaiveLoad |   6.463 ms | 0.0337 ms | 0.0316 ms |  0.13 |    0.00 |
|                                AvxLoad |   1.649 ms | 0.0187 ms | 0.0175 ms |  0.03 |    0.00 |
Ссылка на код неправильная.
Так можно сравнивать что-угодно с чем угодно.

а) не проверять корректность теста, что на выходе правильные данные
б) сравнивать теплое с мягким
Учел критику/пожелания и добавил немного оптимизаций. Бенчмарк в репозитории.

|                                 Method |      Mean | Allocated |
|--------------------------------------- |----------:|----------:|
|          System_Convert_ToBase64String |  48.80 ms |  53.33 MB |
|           System_Memory_Base64ToString |  15.29 ms |  26.67 MB |
|             BaseN_BaseNDecoder_Convert |  49.13 ms |  26.67 MB |
|           BaseN_Base64Convert_ToString |  59.41 ms |  53.33 MB |
|           BaseN_Base32Convert_ToString |  70.00 ms |     64 MB |
| Wiry_Base32Encoding_Standard_GetString | 100.28 ms |    128 MB |
|       SimpleBase_Base32_Rfc4648_Encode | 102.78 ms |     64 MB |
|                  Albireo_Base32_Encode | 152.63 ms |    128 MB |


Сравнивать с System.Buffers.Text.Base64 некорректно т.к. любой general алгоритм будет проигрывать специализированному. У меня принимается словарь на вход, даже пользовательский, а в System.Buffers.Text.Base64 словарь зашит в алгоритм. Плюс там ввернуто много SSE и AVX, которые будет очень сложно навернуть на общий алгоритм.
Сравнивать с System.Buffers.Text.Base64 некорректно т.к. любой general алгоритм будет проигрывать специализированному.

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


У меня принимается словарь на вход, даже пользовательский, а в System.Buffers.Text.Base64 словарь зашит в алгоритм.

И часто ли случается ситуация, когда в качестве алфавита используется что-то отличное от RFC4648? Как мне кажется, это настолько редкая ситуация, что нет смысла из-за неё снижать производительность алгоритма на полтора порядка.

Не вижу никаких проблем вызывать оптимизированный алгоритм при выполнении определённых условий

Полностью согласен. Даже более, я сам использую System.Buffers.Text.Base64 везде, где его можно применить.
Но сравнивать производительность, и говорить «почему ваш общий алгоритм медленнее специализированного» не очень корректно.
И часто ли случается ситуация, когда в качестве алфавита используется что-то отличное от RFC4648?

Да, есть богомерзкий Base64 URL который всё чаще торчит из разных Web API.
снижать производительность алгоритма на полтора порядка

Сейчас в два с половиной раза (табличка вверху). Полтора порядка это 15 раз.
Бенчмарк в репозитории.

Не нашёл изменений в репозитории.


Сейчас в два с половиной раза (табличка вверху). Полтора порядка это 15 раз.

Потому что при использовании System.Buffers.Text.Base64 должно быть около 3 мс, а не 15 мс.

Залил, версия 3.1.0.
Я не смог получить 3мс на кодировании 20мб данных через Base64.EncodeToUtf8. И даже если такое получится, то советую проверить output на то что там действительно правильно закодировалось.

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


BenchmarkDotNet=v0.12.1, OS=arch 
Intel Core i7-2600K CPU 3.40GHz (Sandy Bridge), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=5.0.101
  [Host]     : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
  DefaultJob : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT

|                                 Method |       Mean |     Error |    StdDev | Ratio | RatioSD |    Gen 0 |    Gen 1 |    Gen 2 |   Allocated |
|--------------------------------------- |-----------:|----------:|----------:|------:|--------:|---------:|---------:|---------:|------------:|
|          System_Convert_ToBase64String |  42.735 ms | 0.7655 ms | 0.7160 ms |  1.00 |    0.00 | 166.6667 | 166.6667 | 166.6667 |  55924150 B |
|   System_Memory_Base64ToString_NoAlloc |   4.451 ms | 0.0383 ms | 0.0320 ms |  0.10 |    0.00 |        - |        - |        - |         2 B |
|           System_Memory_Base64ToString |  11.969 ms | 0.2381 ms | 0.2742 ms |  0.28 |    0.01 | 125.0000 | 125.0000 | 125.0000 |  27962095 B |
|             BaseN_BaseNDecoder_Convert |  73.296 ms | 0.5288 ms | 0.4946 ms |  1.72 |    0.04 |        - |        - |        - |  27962097 B |
|           BaseN_Base64Convert_ToString |  80.486 ms | 0.0185 ms | 0.0145 ms |  1.89 |    0.03 |        - |        - |        - |  55924121 B |
|           BaseN_Base32Convert_ToString |  85.490 ms | 0.3087 ms | 0.2737 ms |  2.00 |    0.04 | 142.8571 | 142.8571 | 142.8571 |  67108970 B |
| Wiry_Base32Encoding_Standard_GetString |  89.434 ms | 1.5743 ms | 1.4726 ms |  2.09 |    0.03 | 333.3333 | 333.3333 | 333.3333 | 134217916 B |
|       SimpleBase_Base32_Rfc4648_Encode | 107.411 ms | 0.5663 ms | 0.5297 ms |  2.51 |    0.04 |        - |        - |        - |  67108946 B |
|                  Albireo_Base32_Encode | 149.534 ms | 0.6898 ms | 0.5385 ms |  3.51 |    0.07 | 500.0000 | 500.0000 | 500.0000 | 134217986 B |

Можно, конечно, упирать на то, что память все равно надо выделять. Но вот только что мешает использовать эффективные решения и для этого сценария? (MemoryPool, обработка данных фрагментами и т.д.)

Ну если тестить на кейсе где output летит в трубу, то стоит тестить по честному:

Код (тестилось на версии 3.1.1)
Результат:
|                               Method |      Mean | Ratio |
|------------------------------------- |----------:|------:|
| System_Memory_Base64ToString_NoAlloc |  3.480 ms |  1.00 |
|   BaseN_BaseNDecoder_Convert_NoAlloc | 18.628 ms |  5.36 |


Результаты сравнения с остальными альтернативами. Теперь немного быстрее встроенного Convert.ToBase64String() и много быстрее альтернатив.
В readme указано, что это желаемое (или нежелаемое для некоторых) поведение. Средства валидации присуствуют
GetCharCount(buffer) != GetMaxCharCount(buffer)
для тех кто хочет с этим что-то делать.
Sign up to leave a comment.

Articles