Страницы: 1
RSS
Как правильно формировать строку в цикле, (+ Обсуждение замены Join)
 
Доброго времени суток, Планетяне!

Anchoret тут упомянул тему из архива, в которой приводятся тесты, показывающие превосходство "правильной" сцепки.

Предлагаю перенести эту тему в Курилку.
Тестовый стенд с изменённой версией из #16 архивной темы (там дальше более крутые версии)

P.S.: я до конца принцип не понимаю, поэтому продолжаю использовать Join  :D

P.P.S.: если вместо единичек заполнять массив буквами, то Join проигрывает ещё сильнее, а на 100 млн (у меня даже больше 1 млн не бывает на практике) вылетает в синий экран из-за Out Of Memory
Изменено: Jack Famous - 11.03.2019 18:39:04
Во всех делах очень полезно периодически ставить знак вопроса к тому, что вы с давних пор считали не требующим доказательств (Бертран Рассел) ►Благодарности сюда◄
 
Там на самом деле ничего сложного нет:
- самая долгоиграющая операция - присвоение переменной текстовой строки переменной длины
- вторая по вредности - пришить к одной строке другую посредством "&" или "+" (для строк)
- и в одном и другом случае идет резервирование места в памяти для строки, и чем эта строка больше, тем дольше все это происходит
- в качестве обходного маневра единовременно создается строка определенной длины равной сумме всех подстрок
- и с помощью точечной вставки (Mid(string,start,len)=Mid(string,start,len)) вносятся корректировки, а это уже гораздо быстрее
 
Anchoret, спасибо)) я "на словах" принцип понимаю, НО:

1. для массивов до 1 млн разница несущественна, а больше у меня не бывает
2. для сцепки через Join нужен одномерный массив. У меня многое на них завязано и в большинстве случаев одномерный массив для Join или уже есть или после создания сохраняется для дальнейшего использования (включая глобальные Public-переменные). Разумеется, при таком подходе, никакая "правильная сцепка" не может конкурировать (но это мой частный случай)
3. Насколько стабильна такая методика? Если написать UDF по такому принципу и передавать в неё массив и разделитель, всегда ли результат будет такой же, как со штатной Join?
4. сохранится ли выигрыш в скорости для разделителя из нескольких символов? В последней версии в #21 формируется строка, состоящая из разделителей. Делается это с помощью функции String, которая повторяет только ОДИН символ заданное количество раз. Несколько символов надёжнее, если нужен разделитель, которого точно нет в тексте.

Вот, что меня пока пугает и заботит))) А тема очень крутая (особенно объяснения), поэтому хочется посмотреть на бурное обсуждение и, в итоге, рождение стабильной и шустрой UDFки. Тот факт, что ей можно будет передавать двумерный массив УЖЕ её серьёзно противопоставит Join
Изменено: Jack Famous - 11.03.2019 19:00:04
Во всех делах очень полезно периодически ставить знак вопроса к тому, что вы с давних пор считали не требующим доказательств (Бертран Рассел) ►Благодарности сюда◄
 
Jack Famous, штатный Join быстрее на склейке массивов. Разница отслеживалась до 1кк сцепок. С Join нет таких косяков, как с Replace и Split, две последние писали халтурщики от Microsoft.

Если интересно, то прогоните с таймером код ниже и стандартный Join.
Код
Function aJoin(arr(), del)
Dim a&, b&
For a = LBound(arr) To UBound(arr): b = b + Len(arr(a)): Next
aJoin = Space$(b + (UBound(arr) - LBound(arr)) * Len(del)): b = 1
For a = LBound(arr) To UBound(arr)
  Mid$(aJoin, b, Len(arr(a))) = arr(a): b = b + Len(arr(a))
  If a <> UBound(arr) Then Mid$(aJoin, b, Len(del)) = del: b = b + Len(del)
Next
End Function

Тестер на генерацию энного кол-ва символов. Сначала в массив посимвольно, потом в строке через Space/Mid:
Код
Sub ccc()
Dim dt$, a&, b&, tt#, x&, arr()
'------------------------
 x = 10000000: Randomize
For b = 100000 To x Step 100000
  tt = Timer
  ReDim arr(1 To b)
  For a = 1 To b
    arr(a) = Chr(192 + Int(Rnd * 31))
  Next
  tt = Timer - tt: Debug.Print "String gen. time: " & Format(tt, "0.000") & " Chars: " & UBound(arr)
  tt = Timer
  dt = Join(arr, "")
  tt = Timer - tt: Debug.Print "Join time: " & Format(tt, "0.000") & " Chars: " & UBound(arr)
  tt = Timer
  dt = Space$(b)
  For a = 1 To b
    Mid$(dt, a, 1) = Chr(192 + Int(Rnd * 31))
  Next
  tt = Timer - tt: Debug.Print "Mid/Space - time: " & Format(tt, "0.000") & " String len: " & Len(dt)
Next
End Sub
Изменено: Anchoret - 11.03.2019 19:37:02
 
Anchoret, разница на 1 и 10 млн в 3 раза в пользу Join (на 100 млн Join отработал за 36 сек, aJoin запускать не стал)  :D
Тестовый стенд (1, 2 или 3 символа в разделителе - на скорость не влияет)
, но мне кажется, что это нечестное сравнение, т.к. Join работает ТОЛЬКО с ОДНОМЕРНЫМИ массивами, а "правильную сцепку" можно делать и с ДВУХМЕРНЫМ без потери скорости. В таком случае мои тесты показывают, что Join уже после 10 млн "сдаёт" — с другой стороны и объёмов таких нет (у меня). В общем, как и всегда — баланс "универсальность/скорость/гибкость"  :D

Спасибо за UDF — под двумерный массив переделать несложно и у вас мне оказалось понятнее  :idea:
Изменено: Jack Famous - 12.03.2019 11:00:46
Во всех делах очень полезно периодически ставить знак вопроса к тому, что вы с давних пор считали не требующим доказательств (Бертран Рассел) ►Благодарности сюда◄
 
Есть еще одна интересная тема - байтовое представление текста. Эта штука работает очень быстро.
Помещение в байтовый массив двумя способами строки в 25кк символов.  Время:
Код
String to array time: 0,047 Words: 1000000 Str.Len= 25
StrConv to array time: 0,090 Words: 1000000 Str.Len= 25

Как это выглядит:
Код
Dim bb() as Byte, dt$
dt="......"
bb = dt'здесь на каждый символ приходится два байта (младший, старший), и кодировка - Unicode если к младшему байту прибавить 4
bb = StrConv(dt, 128)'здесь уже все, как надо - один символ - один байт, но дольше

Вот если слияние проводить в байтах, то будет быстрее. Но есть одно НО - чтобы в перевести в байтовый массив нужно считать одномерный массив со строками в одну большую строку...
Изменено: Anchoret - 12.03.2019 16:10:34
 
Anchoret, помню что-то про байты было уже как-то, но я так и не добрался до этой темы…
Во всех делах очень полезно периодически ставить знак вопроса к тому, что вы с давних пор считали не требующим доказательств (Бертран Рассел) ►Благодарности сюда◄
 
Jack Famous, приветствую! Тема старая, но я также задавалась вопросом по замене или оптимизации Join, т.к. есть особенности и ограничения у данного метода! От себя добавлю, и Вам тоже думаю будет интересно:
Цитата
написал:
Есть еще одна интересная тема - байтовое представление текста. Эта штука работает очень быстро.
Согласна быстро, но не достаточно и идея неплохая, но данный способ не учитывает само время преобразования из строки в байты! А вышеперечисленные алгоритмы напрямую работают со строками и лишены этой операции! А чем длиннее строки, тем дольше будет осуществляется это преобразовании строки в байт-массив! Как итог: не всегда работа с байтами быстрее работы с данными на прямую, это касается высокоуровневый кодов!

Мы имеем 3 алгоритма:
Код
Sub ConcatFast()
Dim temp(), x, tm!, i&, j&, v$, line$
If Not GetArray(temp) Then Exit Sub Else tm = Timer: line = " "
    For Each x In temp
        v = x & ";": j = i + Len(v)
        If j > Len(line) Then line = line & Space$(Len(line))
        Mid$(line, i + 1) = v: i = j
    Next x
line = Left$(line, j - 1)
Debug.Print "ConcatFast (Time): " & Format$(Timer - tm, "0.000 ms")
Debug.Print "ConcatFast (Len): " & Len(line)
End Sub
Код
Sub ConcatJoin()
Dim arrNum(), temp(), x, tm!, n&, line$
If Not GetArray(temp) Then Exit Sub Else tm = Timer: ReDim arrNum(0 To (UBound(temp, 1) * UBound(temp, 2) - 1)): n = -1
    For Each x In temp
        n = n + 1: arrNum(n) = x
    Next x
line = Join(arrNum, ";")
Debug.Print "ConcatJoin (Time): " & Format$(Timer - tm, "0.000 ms")
Debug.Print "ConcatJoin (Len): " & Len(line)
End Sub
Код
Function aJoin(arr(), del)
Dim a&, b&
For a = LBound(arr) To UBound(arr): b = b + Len(arr(a)): Next
aJoin = Space$(b + (UBound(arr) - LBound(arr)) * Len(del)): b = 1
    For a = LBound(arr) To UBound(arr)
        Mid$(aJoin, b, Len(arr(a))) = arr(a): b = b + Len(arr(a))
        If a <> UBound(arr) Then Mid$(aJoin, b, Len(del)) = del: b = b + Len(del)
    Next a
End Function

Комментарий по ConcatFast: Всё хорошо и спасибо за данное сконструированное решение, но есть один недочёт, а именно:
Код
If j > Len(line) Then line = line & Space$(Len(line))
У вас в тестах Вы формируете массив из единиц:
Код
Function GetArray(arr()) As Boolean
Dim c&, r&: ReDim arr(1 To 1000000, 1 To 100)
    For c = 1 To UBound(arr, 2)
        For r = 1 To UBound(arr, 1)
            arr(r, c) = 1 ' Я про это!
        Next r
    Next c
GetArray = True
End Function
К сожалению, в большинстве случаев таких данных практически не бывает (Len = 1), поэтому, если Вы увеличите длину тестируемых данных хотя-бы на 1 (Len = 2), то Вы получите некорректные данные на начальном этапе, на выводе:
Код
Function GetArray(arr()) As Boolean
Dim c&, r&: ReDim arr(1 To 1000000, 1 To 100)
    For c = 1 To UBound(arr, 2)
        For r = 1 To UBound(arr, 1)
            arr(r, c) = 1 & "T"
        Next r
    Next c
GetArray = True
End Function
Вывод:
Код
"1T 1  1T 1T;1T;1T;1T;1T;1T;1T...

Это потому, что начальная длина пустой строки, которая впоследствии наращивается имеет начальную длину в один пробел:
Код
line = " "
И при первой же итерации Вам нужно вставить 3 символа ("1T" + разделитель ";"), но строка формируется только на 2 символа:
Код
line = line & Space$(Len(line)) ' line = "  "
И уже при первой итерации, взамен "1T;" мы получаем "1T". Далее данные постепенно нормализуются, но если вставить данные с длиной > 2, например 3 (1TS),то Вы словите ошибку, т.к. метод Mid$ будет вынужден обращаться к несуществующему индексу, ведь длина наращиваемой строки будет меньше, чем требуется!

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

Чтобы исправить данный баг, предлагаю два решения:
  1. На начальном этапе взять длину строки с запасом, чтобы длина line = " " была гарантированно больше первого элемента просматриваемого массива, далее данные нормализуются. Например у Вас первый элемент имеет длину в 12 символов (x(0) = "Hello_Nig..."), значит line = Space(Len(x) * 2). Но для этого необходимо прописывать дополнительный алгоритм на определение длины первого элемента... Поэтому 2 оптимальный, на мой взгляд, вариант;
  2. Внести изменение в строчку и добавить следующее:
    Код
    До:    If j > Len(line) Then line = line & Space$(Len(line))
    После: If j > Len(line) Then line = line & Space$(Len(line) + Len(x))
    

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

Далее ConcatJoin. Сам Join стандартными методами VBA обогнать не удастся! Microsoft постарались и сделали одну из шустрых функций на VBA:
Цитата
написал:
line = Join(arrNum, ";")
Поэтому решающее значение будут иметь входные данные и скорость их преобразования в одномерный массив:
Цитата
написал:
   For Each x In temp
       n = n + 1: arrNum(n) = x
   Next x
Как итог: ConcatJoin самый оптимальный и простой вариант, т.к. в большинстве случаев Вы не будете работать с Big Data через Excel, посредством VBA!


И aJoin. Что-то мне подсказывает, что Anchoret, шарит и увлекался компилируемыми языками, ведь данное решение, на мой взгляд, отлично себя покажет именно в компилируемом варианте (вычислить общую длину получаемой строки, единожды выделить память под данную строку и заполнить её через цикл, без лишнего изменения и обращения к менеджеру памяти OS, с просьбой выдать ещё свободного места😊). Но хочу указать на то, что это только моё предположение и я могу ошибаться!

Что по данному варианту на VBA, то мы имеем современный интерпретируемый вариант реализации языка (компиляция в промежуточное представление (подобие байт-кода) и выполнение на "виртуальной машине" Excel) и в данном случае это решение будет показывать себя с лучшей стороны, только при очень больших массивах данных, т.к. мы тратим наше "драгоценное" время на подсчёт общей длины строки и используем метод Mid$ два раза, взамен одного в методе ConcatFast. Но в методе ConcatFast мы имеем конкатенацию "line & Space$(Len(line))" и чем больше входных данных, тем медленнее будет отрабатывать данный алгоритм, а aJoin наоборот будет улучшать результат по отношению к ConcatFast!

Итог: Берите ConcatJoin и не парьтесь)

Цитата
Укажу на то, что это старая статья (2019 год) и никаких претензий я не Высказываю (на момент публикации я вообще не имела никаких дел с программированием😁)! Только поделилась своими мыслями и указала на недочёт в алгоритме! Буду признательна аргументированной критике в мой адрес, если это имеет место быть!
Изменено: Aнaстaсия - 25.09.2023 05:53:00
 
Aнaстaсия, здравствуйте
Цитата
Aнaстaсия: Как итог: ConcatJoin самый оптимальный и простой вариант, т.к. в большинстве случаев Вы не будете работать с Big Data через Excel, посредством VBA!
тема старая и сейчас ситуация следующая:
    Если мне нужно собрать ОДНУ строку, то я использую сбор в строковый массив с последующим Join.
    Но, если стоит задача типа "СцепитьЕсли", где нужно получить из массива уникальный список ключей и сцепить все значения, соответствующие этим ключам в строку (и вывести напротив ключа. Возможно, с учётом уникальности значений), то тут редимы убьют всю скорость и быстрее просто сцеплять по-старинке: tx = tx & value.
    Возможно в ближайшее время, мне станут доступны новые способы работы со строками — если так и будет, то поделюсь.
Изменено: Jack Famous - 22.09.2023 10:19:27
Во всех делах очень полезно периодически ставить знак вопроса к тому, что вы с давних пор считали не требующим доказательств (Бертран Рассел) ►Благодарности сюда◄
Страницы: 1
Наверх