Хотелось померять скорость вставки строки в разных сценариях с обычной строкой, билдер без выделенного заранее буффера и билдер с буффером.
Если не настраивать параметры теста, то по умолчанию он начинает длинные танцы с бубном, я так и не дождался результата за 5 минут и не понятно когда ждать завершения — прервал.
Если настраивать на минимализм, то выдает результат быстро но не нравится погрешность результата
и немного странно что пустой метод работает медленнее чем вставка ( на самом деле высокая погрешность ), погрешность у вставки могу понять — требуется выделение памяти и оно сильно зависит от разных условий.
Но вот почему пустой метод имеет такую высокую погрешность не понятно.
| Method | Mean | Error | StdDev | Median | Min | Max |
|-------------------------------- |---------:|----------:|----------:|----------:|----------:|---------:|
| InsertStr | 41.84 us | 183.25 us | 121.21 us | 3.6000 us | 2.9000 us | 386.8 us |
| InsertStringBuilder | 95.50 us | 446.81 us | 295.54 us | 1.8500 us | 0.7000 us | 936.6 us |
| InsertStringBuilderWithCapacity | 41.91 us | 192.41 us | 127.27 us | 1.4500 us | 0.7000 us | 404.1 us |
| Empty | 81.96 us | 390.78 us | 258.48 us | 0.2000 us | 0.1000 us | 817.6 us |
Вопрос — какие настройки оптимальны по точности и скорости выполнения теста ( хотелось чтобы тест выполнялся не более минуты )
[SimpleJob(RunStrategy.ColdStart, launchCount: 1, warmupCount: 0, targetCount: 10)]
[MinColumn, MaxColumn, MeanColumn, MedianColumn]
public class BenchmarkTest
{
string str;
StringBuilder stringBuilder;
StringBuilder stringBuilderWithCapacity;
const string baseStr = "1234567890";
const int repeatCount = 10;
public BenchmarkTest()
{
str = string.Join(string.Empty, Enumerable.Repeat(baseStr, repeatCount));
stringBuilder = new StringBuilder(str);
stringBuilderWithCapacity = new StringBuilder(baseStr.Length * (repeatCount + 1));
stringBuilderWithCapacity.Append(str);
}
[Benchmark]
public void InsertStr()
{
str.Insert(4, "5");
}
[Benchmark]
public void InsertStringBuilder()
{
stringBuilder.Insert(4, "5");
}
[Benchmark]
public void InsertStringBuilderWithCapacity()
{
stringBuilderWithCapacity.Insert(4, "5");
}
[Benchmark]
public void Empty()
{
}
}
”Жить стало лучше... но противнее. Люди которые ставят точку после слова лучше становятся сторонниками Путина, наши же сторонники делают акцент на слове противнее ( ложь, воровство, лицемерие, вражда )." (с) Борис Немцов
Здравствуйте, okon, Вы писали:
O>Хотелось померять скорость вставки строки в разных сценариях с обычной строкой, билдер без выделенного заранее буффера и билдер с буффером. O>Если не настраивать параметры теста, то по умолчанию он начинает длинные танцы с бубном, я так и не дождался результата за 5 минут и не понятно когда ждать завершения — прервал.
Не очень понятно, что именно вы хотите измерить.
BDN предполагает, что замеряемая операция воспроизводима.
Т.е. что-то типа "берём пустой stringbuilder и вставляем в него 100 символов по одному", сравниваем с "берём stringbuilder с capacity=100 и вставляем в него 100 символов по одному".
То, что у вас написано — это стрингбилдер инициализируется 1 раз, затем в него охулиард раз вставляется 1 символ.
Понятно, что перформанс этой вставки зависит не от подробностей инициализации, а от посторонних вещей — типа пришлось ли сделать GC при перевыделении буфера во время именно этой вставки.
Я бы писал примерно так:
public class BenchmarkTest
{
[Param(1000)]
public int InsertCount{get;set;}
[Param(10)]
public int RepeatCount{get;set;}
[Param("1234567890")]
public string BaseString{get;set;}
[Benchmark]
public void InsertStr()
{
var str = GetString();
for(var i = 0; i<InsertCount; i++)
str = str.Insert(4, "5");
}
[Benchmark]
public void InsertStringBuilder()
{
stringBuilder = new StringBuilder(GetString());
for(var i=0; i<InsertCount; i++)
stringBuilder.Insert(4, "5");
}
[Benchmark]
public void InsertStringBuilderWithCapacity()
{
var stringBuilderWithCapacity = new StringBuilder(BaseStr.Length * RepeatCount + InsertCount);
stringBuilderWithCapacity.Append(GetString());
for(var i=0; i<InsertCount; i++)
stringBuilderWithCapacity.Insert(4, "5");
}
[Benchmark]
public void Empty()
{
}
private string GetString()=> string.Join(string.Empty, Enumerable.Repeat(BaseStr, RepeatCount));
}
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали: S>Здравствуйте, okon, Вы писали: O>>Хотелось померять скорость вставки строки в разных сценариях с обычной строкой, билдер без выделенного заранее буффера и билдер с буффером. O>>Если не настраивать параметры теста, то по умолчанию он начинает длинные танцы с бубном, я так и не дождался результата за 5 минут и не понятно когда ждать завершения — прервал. S>Не очень понятно, что именно вы хотите измерить. S>BDN предполагает, что замеряемая операция воспроизводима.
Хотелось задать строку определенной длины например 100 символов и посмотреть сколько будет стоить вставка строки в 1 символ. S>Т.е. что-то типа "берём пустой stringbuilder и вставляем в него 100 символов по одному", сравниваем с "берём stringbuilder с capacity=100 и вставляем в него 100 символов по одному". S>То, что у вас написано — это стрингбилдер инициализируется 1 раз, затем в него охулиард раз вставляется 1 символ.
Во как, я просто привык что в тестах обычно на каждый вызов метод создается новый экземпляр.
Тут видимо иначе, в документации я нашел пример с конструктором, но там не уточняется как этот экземпляр используется. https://benchmarkdotnet.org/articles/overview.html
p.s. проверил экспериментально — да конструктор 1 раз вызывается на все замеры а не перед каждым замером, печально придется как-то обходить.
S>Понятно, что перформанс этой вставки зависит не от подробностей инициализации, а от посторонних вещей — типа пришлось ли сделать GC при перевыделении буфера во время именно этой вставки.
S>Я бы писал примерно так: S>[cs] S> [Benchmark] S> public void InsertStringBuilderWithCapacity() S> { S> var stringBuilderWithCapacity = new StringBuilder(BaseStr.Length * RepeatCount + InsertCount); S> stringBuilderWithCapacity.Append(GetString()); S> for(var i=0; i<InsertCount; i++) S> stringBuilderWithCapacity.Insert(4, "5"); S> }
Да но в этом случае тест получается не совсем корректный — тут мы измеряем не только вставку но и создание экземпляра StringBuilder и копирование в него строки.
А хочется замерить только вставку в уже проинициализированный. Можно конечно создать дополнительные методы без вставки и сравнить время, но это как временный вариант — не уверен что это правильный способ.
p.s. попробовал сделать замер с методами инициализации и вставки получается тоже высокая погрешность и странности например метод InsertStringBuilder () меньше погрешность чем у метода InitStringBuilder (), хотя insert состоит из 2х частей такой же init и непосредственно операция вставки. Но есть и плюсы — метод Empty стал адекватнее измеряться.
т.е. получаем операции
string.Insert = 110 +- 40 us
StringBuilderInsert = 70+-10 us
StringBuilderInsert(WithCapacity) = 50+-10us
Что впринципе логично и объяснимо.
У нас это называлась подгонка результата ))
”Жить стало лучше... но противнее. Люди которые ставят точку после слова лучше становятся сторонниками Путина, наши же сторонники делают акцент на слове противнее ( ложь, воровство, лицемерие, вражда )." (с) Борис Немцов
Здравствуйте, okon, Вы писали:
S>>Не очень понятно, что именно вы хотите измерить. S>>BDN предполагает, что замеряемая операция воспроизводима.
O>Хотелось задать строку определенной длины например 100 символов и посмотреть сколько будет стоить вставка строки в 1 символ.
Слишком большой разброс получится.
S>>Т.е. что-то типа "берём пустой stringbuilder и вставляем в него 100 символов по одному", сравниваем с "берём stringbuilder с capacity=100 и вставляем в него 100 символов по одному".
O>Во как, я просто привык что в тестах обычно на каждый вызов метод создается новый экземпляр.
Микробенчмарк так не сделаешь — время вызова обычно пренебрежимо мало по сравнению со временем создания экземпляра. O>Тут видимо иначе, в документации я нашел пример с конструктором, но там не уточняется как этот экземпляр используется. O>p.s. проверил экспериментально — да конструктор 1 раз вызывается на все замеры а не перед каждым замером, печально придется как-то обходить.
Надо читать https://benchmarkdotnet.org/articles/features/setup-and-cleanup.html
O>Да но в этом случае тест получается не совсем корректный — тут мы измеряем не только вставку но и создание экземпляра StringBuilder и копирование в него строки. O>А хочется замерить только вставку в уже проинициализированный. Можно конечно создать дополнительные методы без вставки и сравнить время, но это как временный вариант — не уверен что это правильный способ.
Правильного способа нет. Можно использовать IterationSetup, но авторы не рекомендуют пользоваться им, если измеряемый метод работает меньше 100ms.
O>Что впринципе логично и объяснимо. O>У нас это называлась подгонка результата ))
Вы выбрали сложный эксперимент. Вся закавыка — в том, что у измеряемой операции нет какого-то объективного времени выполнения.
Стоимость вставки одного символа зависит от слишком многих параметров — от длины строки, от позиции вставки, от состояния хипа в момент вставки, и т.д.
Независимо от того, какой методикой делать бенчмарк, подготовка воспроизводимости стоит слишком много по сравнению с самой операцией.
Поэтому придётся либо смириться с никакой точностью, либо подменить задачу на более удобную — например, замерить стоимость вставки не одного символа, а десятка тысяч по-символьных вставок.
Тогда можно говорить об "амортизированной стоимости" одной операции, поделив общее время на 10000.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, StatujaLeha, Вы писали:
SL>Здравствуйте, okon, Вы писали:
SL>Оно?
SL> [IterationSetup] SL> public void SetUp() SL> { SL> this.stringBuilder = new StringBuilder(BaseString, 1024); SL> }
SL>| Type | Method | Mean | Error | StdDev | Median | SL>|------------------------------------ |------- |---------:|----------:|----------:|---------:| SL>| InsertFullStringBuilderBenchmark | Insert | 6.761 us | 14.576 us | 42.979 us | 1.600 us | SL>| InsertNotFullStringBuilderBenchmark | Insert | 6.227 us | 12.863 us | 37.926 us | 2.300 us |
SL>[/cs]
Да структура кода похожа на то что хотелось бы, но вот в паралельной ветке ответили что InterationSetup не рекомендуется к использованию.
И вот результаты измерений не хорошие получаются — погрешность в 2 раза выше чем само значение.
”Жить стало лучше... но противнее. Люди которые ставят точку после слова лучше становятся сторонниками Путина, наши же сторонники делают акцент на слове противнее ( ложь, воровство, лицемерие, вражда )." (с) Борис Немцов
O>>Что впринципе логично и объяснимо. O>>У нас это называлась подгонка результата )) S> Вы выбрали сложный эксперимент. Вся закавыка — в том, что у измеряемой операции нет какого-то объективного времени выполнения. S>Стоимость вставки одного символа зависит от слишком многих параметров — от длины строки, от позиции вставки, от состояния хипа в момент вставки, и т.д.
Согласен.
S>Независимо от того, какой методикой делать бенчмарк, подготовка воспроизводимости стоит слишком много по сравнению с самой операцией. S>Поэтому придётся либо смириться с никакой точностью, либо подменить задачу на более удобную — например, замерить стоимость вставки не одного символа, а десятка тысяч по-символьных вставок. S>Тогда можно говорить об "амортизированной стоимости" одной операции, поделив общее время на 10000.
Вот честно говоря я думал что бенчмарк так и работает — т.е. выполняет метод много раз и получает некое амортизированное значение
т.е. представим что в бенчмарке есть цикл
включаем секундомер
от 1 до N
{
меряй меня
}
выключаем секундомер
а я вставлю внутри еще один цикл
...
от 1 до N
{
от 1 до M
меряй меня
}
....
то это будет эквивалентно если в бенчмарке задать от 1 до N * M
если же там
от 1 до N
включаем секундомер
{
меряй меня
}
ставим на паузу секундомер
то тогда да если метод очень короткий — на грани точности секундомера то ничего не померить будет — но не понятно зачем так делать.
Лучше было бы замерить отдельно пустой цикл и потом вычесть разницу.
”Жить стало лучше... но противнее. Люди которые ставят точку после слова лучше становятся сторонниками Путина, наши же сторонники делают акцент на слове противнее ( ложь, воровство, лицемерие, вражда )." (с) Борис Немцов
Здравствуйте, okon, Вы писали:
O>Согласен.
O>Вот честно говоря я думал что бенчмарк так и работает — т.е. выполняет метод много раз и получает некое амортизированное значение
Не совсем так. Он выполняет метод много раз, но количество раз он подбирает так, чтобы получить заданную точность.
Если у нас повторные вызовы метода занимают всё больше и больше времени, то BDN будет растерян, т.к. измеряемая величина не сходится ни к чему конкретному. O>т.е. представим что в бенчмарке есть цикл O>
O>включаем секундомер
O>от 1 до N
O> {
O> меряй меня
O> }
O>выключаем секундомер
O>
O>а я вставлю внутри еще один цикл
O>
O>...
O>от 1 до N
O> {
O> от 1 до M
O> меряй меня
O> }
O>....
O>
O>то это будет эквивалентно если в бенчмарке задать от 1 до N * M
Нет — N в этом случае величина плавающая; BDN пытается её угадать методом подбора.
А проблема — в том, что "меряй меня" выполняется в заведомо различных стартовых условиях. O>если же там O>
O>от 1 до N
O> включаем секундомер
O> {
O> меряй меня
O> }
O> ставим на паузу секундомер
O>
O>то тогда да если метод очень короткий — на грани точности секундомера то ничего не померить будет — но не понятно зачем так делать. O>Лучше было бы замерить отдельно пустой цикл и потом вычесть разницу.
Так и делается.
Нормальный способ для вашего случая — именно такой:
от 1 до N
сбрасываем настройки
включаем секундомер
{
от 1 до M
меряй меня
}
ставим на паузу секундомер
Тогда каждая итерация будет делать одинаковую работу (ну, плюс-минус). При работе с ВDN лучше не делать никаких предположений об N.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали: S>Так и делается. S>Нормальный способ для вашего случая — именно такой: S>
S>от 1 до N
S> сбрасываем настройки
S> включаем секундомер
S> {
S> от 1 до M
S> меряй меня
S> }
S> ставим на паузу секундомер
S>
S>Тогда каждая итерация будет делать одинаковую работу (ну, плюс-минус). При работе с ВDN лучше не делать никаких предположений об N.
Попробовал такой вариант, интересные получились результаты, погрешности ок — порядка 1-2% от значения,
одинаковые значения с capacity и без — тоже понятны, т.к. билдер с недостатком памяти сделать всего 1 раз перевыделение памяти — это будет не заметно.
но странно что вставка string.Insert получилась быстрее чем StringBuilder.Insert
public class BenchmarkInsertCharTest
{
[Params(1000)]
public int IterationCount { get; set; }
[Params(10)]
public int RepeatCount { get; set; }
[Params("1234567890")]
public string BaseString { get; set; }
[Benchmark]
public void InsertStr()
{
var str = GetString();
for (int i = 0; i < IterationCount; i++)
str = str.Insert(4, "5");
}
[Benchmark]
public void InitStr()
{
var str = GetString();
for (int i = 0; i < IterationCount; i++)
{ }
}
[Benchmark]
public void InsertStringBuilder()
{
var stringBuilder = new StringBuilder(GetString());
for (int i = 0; i < IterationCount; i++)
stringBuilder.Insert(4, "5");
}
[Benchmark]
public void InitStringBuilder()
{
var stringBuilder = new StringBuilder(GetString());
for (int i = 0; i < IterationCount; i++)
{
}
}
[Benchmark]
public void InsertStringBuilderWithCapacity()
{
var stringBuilderWithCapacity = new StringBuilder(BaseString.Length * (IterationCount + 1));
stringBuilderWithCapacity.Append(GetString());
for(int i = 0; i < IterationCount; i++)
stringBuilderWithCapacity.Insert(4, "5");
}
[Benchmark]
public void InitStringBuilderWithCapacity()
{
var stringBuilderWithCapacity = new StringBuilder(BaseString.Length * (IterationCount + 1));
stringBuilderWithCapacity.Append(GetString());
for (int i = 0; i < IterationCount; i++)
{
}
}
[Benchmark]
public void Empty()
{
}
private string GetString() => string.Join(string.Empty, Enumerable.Repeat(BaseString, RepeatCount));
}
”Жить стало лучше... но противнее. Люди которые ставят точку после слова лучше становятся сторонниками Путина, наши же сторонники делают акцент на слове противнее ( ложь, воровство, лицемерие, вражда )." (с) Борис Немцов
Здравствуйте, okon, Вы писали:
O>Да структура кода похожа на то что хотелось бы, но вот в паралельной ветке ответили что InterationSetup не рекомендуется к использованию. O>И вот результаты измерений не хорошие получаются — погрешность в 2 раза выше чем само значение.
Да, доработал тест.
[SimpleJob(RunStrategy.ColdStart, RuntimeMoniker.NetCoreApp31)]
public class InsertFullStringBuilderBenchmark
{
public static readonly string BaseString = new string('x', 100);
private StringBuilder[] stringBuilder;
[IterationSetup]
public void SetUp()
{
this.stringBuilder = Enumerable.Repeat(1, 1000000).Select(e => new StringBuilder(BaseString)).ToArray();
GC.Collect();
GC.WaitForPendingFinalizers();
}
[Benchmark]
public void Insert()
{
for (int i = 0; i < this.stringBuilder.Length; ++i)
{
this.stringBuilder[i].Insert(4, "y");
}
}
}
[SimpleJob(RunStrategy.ColdStart, RuntimeMoniker.NetCoreApp31)]
public class InsertNotFullStringBuilderBenchmark
{
public static readonly string BaseString = new string('x', 100);
private StringBuilder[] stringBuilder;
[IterationSetup]
public void SetUp()
{
this.stringBuilder = Enumerable.Repeat(1, 1000000).Select(e => new StringBuilder(BaseString, 1024)).ToArray();
GC.Collect();
GC.WaitForPendingFinalizers();
}
[Benchmark]
public void Insert()
{
for (int i = 0; i < this.stringBuilder.Length; ++i)
{
this.stringBuilder[i].Insert(4, "y");
}
}
}
Результаты только странные...
Runtime=.NET Core 3.1 InvocationCount=1 RunStrategy=ColdStart
UnrollFactor=1
| Type | Method | Mean | Error | StdDev | Median |
|------------------------------------ |------- |---------:|---------:|---------:|---------:|
| InsertFullStringBuilderBenchmark | Insert | 446.5 ms | 30.01 ms | 88.47 ms | 405.8 ms |
| InsertNotFullStringBuilderBenchmark | Insert | 732.8 ms | 14.56 ms | 41.07 ms | 725.2 ms |
SL>Runtime=.NET Core 3.1 InvocationCount=1 RunStrategy=ColdStart
SL>UnrollFactor=1
SL>| Type | Method | Mean | Error | StdDev | Median |
SL>|------------------------------------ |------- |---------:|---------:|---------:|---------:|
SL>| InsertFullStringBuilderBenchmark | Insert | 446.5 ms | 30.01 ms | 88.47 ms | 405.8 ms |
SL>| InsertNotFullStringBuilderBenchmark | Insert | 732.8 ms | 14.56 ms | 41.07 ms | 725.2 ms |
SL>
Да предварительно создать все объекты в массиве хорошая идея.
Почему без Capacity получается почти в 1.5 раза быстрее — тоже пока не понятно.
”Жить стало лучше... но противнее. Люди которые ставят точку после слова лучше становятся сторонниками Путина, наши же сторонники делают акцент на слове противнее ( ложь, воровство, лицемерие, вражда )." (с) Борис Немцов