Страницы: 1
RSS
Почему не работает Join ?
 
Уже час бьюсь над простейшей задачей: слепить UDF, которая будет сцеплять в одну строку стринги из диапазона ячеек-аргументов с заданным разделителем перечисления (в т.ч. и с переводом строки)…  
У меня давно лежит в "копилке" такая UDF с циклом-перебором всех ячеек.  
Решил, что так не красиво и надо бы упростить до безобразия, использовав Join    
Function СКЛЕИТЬ(ДИАПАЗОН, Optional Разделитель$ = "", Optional Переносить As Boolean = True) As String  
' Purpose      : склеить тексты из выделенных ячеек в одну строку с задаваемыми при необходимости разделителями данных из разных ячеек  
' Notes        : по умолчанию включен перенос строк внутри ячейки  
  Разделитель = Разделитель & IIf(Переносить, vbLf, "")  
  СКЛЕИТЬ = Join(ДИАПАЗОН.Value, Разделитель)  
End Function  
Не работает, зараза! Никак не пойму почему? Наверное, конец рабочего дня, мозги "замылились" :(
С уважением, Алексей (ИМХО: Excel-2003 - THE BEST!!!)
 
?  
Алексей, у меня в 2003-м в справке написано, что функция join() ждёт одномерный массив, а у тебя ДИАПАЗОН.Value - двумерный
фрилансер Excel, VBA - контакты в профиле
"Совершенствоваться не обязательно. Выживание — дело добровольное." Э.Деминг
 
ДИАПАЗОН.Value ведь двумерный будет.  
Ты ему transpose накати поверх (2х).  
Вроде так...
 
Спасибо, ребята!  
Всю репу себе проскрёб...
С уважением, Алексей (ИМХО: Excel-2003 - THE BEST!!!)
 
Привет, Алексей. Ребята правильно говорят, дело в измерениях массива. Возможно параллельных ^_^  
 
Вот пример UDF. Работу не проверял.  
 
Function io(rng As Range) As String  
   Dim x, z(), i As Long  
   ReDim z(1 To rng.Cells.Count)  
   For Each x In rng.Value  
       i = i + 1  
       z(i) = x  
   Next  
   io = Join(z, ";")  
End Function
Чебурашка стал символом олимпийских игр. А чего достиг ты?
Тишина - самый громкий звук


https://github.com/nervgh
 
nerv, при таком подходе Join() не нужна, быстрее сразу соединять в цикле, нет? :)
KL
 
С Application.Transpose() тоже не все гладко :)  
 
- она допускает длину строк до 255  
- требует однократного применения для вертикального вектора    
Application.Transpose([A1:A10])
- и двукратного для горизонтального    
Application.Transpose(Application.Transpose([A1:J1]))
 
Так что выбирай, но осторожно.., но выбирай... :)
KL
 
{quote}{login=KL}{date=24.02.2012 06:13}{thema=}{post}nerv, при таком подходе Join() не нужна, быстрее сразу соединять в цикле, нет? :){/post}{/quote}  
Нет. Проверьте сами. Первый макрос у меня выполнился за ~40 сек, результата выполнения второго я не дождался. Объяснить, почему так?  
 
Sub io()  
   Dim t As Single  
   Dim x, z(), i As Long, line As String  
   With [a1.iv65536]
       .Value = 1  
       t = Timer  
       ReDim z(1 To .Cells.Count)  
       For Each x In .Value  
           i = i + 1  
           z(i) = x  
       Next  
       line = Join(z, ";")  
       MsgBox Timer - t  
   End With  
End Sub  
 
 
Sub io2()  
   Dim t As Single  
   Dim x, line As String  
   With [a1.iv65536]
       .Value = 1  
       t = Timer  
       For Each x In .Value  
           line = line & ";" & x  
       Next  
       MsgBox Timer - t  
   End With  
End Sub
Чебурашка стал символом олимпийских игр. А чего достиг ты?
Тишина - самый громкий звук


https://github.com/nervgh
 
nerv, вижу, что неправ. Объяснить :)
KL
 
наверное, что-нибудь, связанное с многократным перевыделением памяти под постоянно удлинняющуюся строку?
фрилансер Excel, VBA - контакты в профиле
"Совершенствоваться не обязательно. Выживание — дело добровольное." Э.Деминг
 
А нето, чтобы Join() делает то же самое внутри, хоть и быстрее из-за скомпиллированности? Ведь я тоже не дождался хоть очень старался :)
KL
 
а м.б. join() сначала заценивает длину будущей строки как сумму длин элементов массива плюс разделители, один раз выделяет память, а потом уж конкатенирует?
фрилансер Excel, VBA - контакты в профиле
"Совершенствоваться не обязательно. Выживание — дело добровольное." Э.Деминг
 
я неправильно выразился :)  
 
не конкатенирует, а просто поочередно запихует байты элементов массива и разделителей в байты результирующей строки, т.е. внутри join() может и не быть никаких операций со строками как таковыми, а только с байтами.  
(конечно, всё это предположения и фантазии, никаких особых фактов для них нет. кроме того, что join() работает быстро :)
фрилансер Excel, VBA - контакты в профиле
"Совершенствоваться не обязательно. Выживание — дело добровольное." Э.Деминг
 
{quote}{login=ikki}{date=24.02.2012 10:37}{thema=}{post}а м.б. join() сначала заценивает длину будущей строки как сумму длин элементов массива плюс разделители, один раз выделяет память, а потом уж конкатенирует?{/post}{/quote}Не похоже. Я смотрел выделение памяти в диспетчере задач (см. скриншот) - во время выполнения join занятый объем памяти возрастает.  
Я запустил процедуру io 2 раза: первый раз как есть, второй раз z$().  
Видно, что во втором случае расходуется меньше памяти, время выполнения на 8% меньше.  
Я экспериментировал с диапазоном [a1.iv40000], на 65536 памяти не хватает.
 
Картинку забыл
 
Если правильно делать конкатенацию в цикле, то Join отдыхает:  
 
' Правильная конкатенация в цикле  
Sub FastConcat()  
 Dim x, t!, i&, j&, s$, v$  
 With Range("A1:Z65536")  
   .Value = 1  
   t = Timer  
   s = " "  
   For Each x In .Value  
     v = x & ";"  
     j = i + Len(v)  
     If j > Len(s) Then s = s & Space$(Len(s))  
     Mid$(s, i + 1) = v  
     i = j  
   Next  
   s = Left$(s, j - 1)  
   Debug.Print Timer - t  
 End With  
End Sub  
 
' Вариант с Join  
Sub io()  
 Dim t As Single  
 Dim x, z(), i As Long, line As String  
 With [A1:Z65536]
   .Value = 1  
   t = Timer  
   ReDim z(1 To .Cells.Count)  
   For Each x In .Value  
     i = i + 1  
     z(i) = x  
   Next  
   line = Join(z, ";")  
   Debug.Print Timer - t  
 End With  
End Sub  
 
В коде для удобства тестирования я уменьшил диапазон ячеек, но это не принципиально.  
Когда то тестировал и на текстах в сотню мегабайт, лишь бы хватало памяти компьютера и Excel-я.  
 
Пояснение.  
 
Когда-то я уже объяснял здесь, что при каждой конкатенации, например, line = line & ";" & x в памяти ищется непрерывный свободный кусок для нового значения, туда переписывается новое значение, а затем освобождается память фрагмена, занимаемого раньше.  Это занимает тем большее время, чем больше длина создаваемой новой строки.  
 
Причем адреса этих фрагментов памяти StrPtr(Line) всегда разные, а адрес указателя VarPtr(line) на фрагмент с текстом остается неизменным.  Вот доказательство:  
 
Sub Test()  
 ' VarPtr(txt) - адрес памяти переменной  txt  
 ' StrPtr(txt) - адрес памяти текста, на который указывает переменная txt  
   
 Dim txt As String  
   
 txt = "1"  
 Debug.Print txt, VarPtr(txt), StrPtr(txt)  
   
 txt = "1" & ";"  
 Debug.Print txt, VarPtr(txt), StrPtr(txt)  
 
End Sub  
   
 
В варианте FastConcat сначала создается буферная строка с любым содержимым, с одним пробелом в данном случае.  
Затем с помощью Mid$(s, i + 1) = v  производится копирование фрагмента памяти, занимаемым v в нужную позицию (учитывается счетчиком i) строки s, при этом строка s остается на своем месте в памяти без переноса в другое место памяти (memory reallocation).  
 
Если длины буфера s недостаточна, тогда буфер наращивается с увеличением длины вдвое (справа добавляются, например, пробелы). При этом, естественно, происходит memory reallocation, но в силу геометрической прогрессии увеличения длины и быстрого выполнения конкатенации при малой длине буфера (напомню, что тормоза – только при большой длине буфера s) общее замедление за счет такого редкого наращивания незначительно.
 
Нет сомнений, что в Join использован подобный алгоритм, для языка C, на котором написаны встроенные функции Excel, это вполне себе популярный метод.  
 
Но когда-то давно, когда луна была сыром, а компьютеры имели мылый объем памяти 640 КБ, оптимизировали код с учетом ограничений памяти.  
И для Join, по все видимости, вместо геометрической прогресси была использована арифметическая. То есть, производится не удвоение длины буфера, а прибавление, например, 32 кило Байт.  
 
Если использовать такой строку в коде FastConcat, то получим примерно ту же скорость, что и Join:  
If j > Len(s) Then s = s & Space$(32768)
 
Всем привет!  
 
Все уже рассказали, и даже сделали это больше и лучше, чем смог бы я : )  
Мои тесты FastConcat и Join на диапазоне Range("A1:iv65536") показали следующие результаты:  
FastConcat - 31,53125  
Join - 33,53125  
Поэтому, я бы не стал утверждать, что "Join отдыхает")  
 
Кстати, ZVI, там немного надо поправить FastConcat, вероятно начальный размер буфера. При длинах строк >= 3 вываливается ошибка, а при длине строки равной 2 конкатенация происходит неправильно.  
 
 
Ну по большому счету достаточно запомнить одно: конкатенация строк в таком вот виде [line = line & ";" & x] - достаточно медленная операция.
Чебурашка стал символом олимпийских игр. А чего достиг ты?
Тишина - самый громкий звук


https://github.com/nervgh
 
На моем компе с MSO2010 Join даже несколько выигрывает. Для Range("A1:iv65536")результаты 5 промеров следующие:  
 
20.07422 (FastConcat)  
19.48047 (io)  
 
Учитывая то, что решение с Join включает еще и дополнительный перебор для формирования одномерного массива, если переставить строку t = Timer непосредственно перед Join, то получается сильно быстрее:  
 
11.39844
KL
 
Любопытно, что MS в свое время либо не смог либо не захотел предложить оптимизированную замену Join для версии 97 (http://support.microsoft.com/default.aspx?scid=kb;en-us;188007)  
 
 
     Public Function Join(source() As String, Optional _  
           sDelim As String = " ") As String  
     Dim sOut As String, iC As Integer  
     On Error GoTo errh:  
         For iC = LBound(source) To UBound(source) - 1  
             sOut = sOut & source(iC) & sDelim  
         Next  
         sOut = sOut & source(iC)  
         Join = sOut  
         Exit Function  
     errh:  
         Err.Raise Err.Number  
     End Function  
 
Если исправить тип переменной iC на Long, то функция будет работать для массивов с более чем 32.767 элементами, но с тем же успехом, что io2 от nerv :)
KL
 
Ну, так c FastConcat есть куда расти, а c Join? :-)  
 
Диапазон ограничен A1:IV40000, чтобы не зависало в Excel 2003, 2007.  
 
Вот так FastConcat примерно в 1.5 раза быстрее, чем io  
 
' Правильная конкатенация в цикле  
Sub FastConcat()  
 Dim x, t!, i&, j&, s$, v$  
 With Range("A1:IV40000")  
   .Value = 1  
   t = Timer  
   Const Delim$ = ";"  
   s = String(32768, Delim)  
   For Each x In .Value  
     v = x  
     j = i + Len(v)  
     If j > Len(s) Then s = s & String(Len(s), Delim)  
     Mid$(s, i + 1) = v  
     i = j + 1  
   Next  
   s = Left$(s, i - 1)  
   Debug.Print Timer - t  
 End With  
End Sub  
 
' Вариант с Join  
Sub io()  
 Dim t As Single  
 Dim x, z(), i As Long, line As String  
 With Range("A1:IV40000")  
   .Value = 1  
   t = Timer  
   ReDim z(1 To .Cells.Count)  
   For Each x In .Value  
     i = i + 1  
     z(i) = x  
   Next  
   line = Join(z, ";")  
   Debug.Print Timer - t  
 End With  
End Sub
 
Владимир, однако, хитрый способ сразу создать строку из нужных разделителей : )  
Вот эта строчка не перестает меня радовать - Mid$(s, i + 1) = v.  
 
Мои тесты на Range("A1:IV40000")  
16,29688 - FContact  
21,3125 - Join  
15,40625 - FContact  
20,6875 - Join  
15,875 - FContact  
20,39063 - Join  
 
на Range("A1:IV65536")  
25,34375 - FContact  
31,76563 - Join  
 
в среднем на 20-25% быстрее.  
 
>Ну, так c FastConcat есть куда расти, а c Join? :-)  
С join пусть растет Microsoft )
Чебурашка стал символом олимпийских игр. А чего достиг ты?
Тишина - самый громкий звук


https://github.com/nervgh
 
У меня разница колеблется между 18% и 19%. На мой взгляд, не смотря на то, что в целом решение Владимира быстрее, если брать отдельно конкатенацию, то в Join она происходит быстрее (+41%), и это интригует :)    
 
9.976563 FastConcat    
11.82031 io  
7.042969 io (только Join)  
 
Кстати, вопрос. А почему:    
 
s = String(32768, Delim)  
 
а не:  
 
s = String(.Count, Delim)  
 
?
KL
 
KL, мне, кажется, потому, что количество ячеек, зачастую, не совпадает с длинами строк в них содержащихся.
Чебурашка стал символом олимпийских игр. А чего достиг ты?
Тишина - самый громкий звук


https://github.com/nervgh
Страницы: 1
Наверх