Comments 21
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 и получить поддержку стримов. Я скорее всего скоро это сделаю у себя в либе в ближайшее время.
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() я не могу поместить в свою библиотеку т.к. это вне зоны ее ответственности.
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мс на кодировании 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, обработка данных фрагментами и т.д.)
Код (тестилось на версии 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() и много быстрее альтернатив.
Настораживает отсутствие валидации входных данных. Это must have для практического использования.
Base64, Base32 и Base16 кодировки в .NET