Оптимизация поиска подстроки

Тема в разделе "WASM.A&O", создана пользователем cresta, 6 янв 2005.

  1. cresta

    cresta Active Member

    Публикаций:
    0
    Регистрация:
    13 июн 2004
    Сообщения:
    2.257
    Приветствую всех.

    В masm32lib есть процедура IsString, да и по сайту нашел несколько примеров, но в них есть некоторые особенности (например, регистрозависимые, удаляющие пробелы, возвращающие ошибку, если искомая строка равна по длине той в которой ищут), которые меня не устраивают, поэтому написал сам процедуру поиска подстроки. Для сравнения взял InString.

    Вот сама процедура:


    Код (Text):
    1. InStringI proc uses ebx esi edi Start:DWORD, StrAddr:DWORD, MatchAddr:DWORD, MatchLen:DWORD
    2.  
    3.     ;--------------------------------------------
    4.     mov ecx,Start                          ;стартовая позиция (поиск с начала строки - 1)        
    5.     mov esi,StrAddr                        ;начало строки
    6.     @StartPos:                             ;поиск нуля в куске строки от начало+старт.позиция
    7.     cmp byte ptr [esi+ecx-1],0             ;до начала строки
    8.     je @OutOfString                        ;если встречен ноль - старт. позиция задана за пределами строки
    9.     loopnz @StartPos               
    10.    
    11.     ;--------------------------------------------
    12.     cmp Start,0                            ;"защита от дурака"
    13.     jle @InvalidStart
    14.  
    15.     mov ecx,MatchLen                       ;кол-во символов, которое должно совпасть
    16.     dec Start
    17.     add esi,Start                          ;абсолютный стартовый адрес
    18.     mov edi,MatchAddr                      ;стартовый адрес MatchString
    19.     xor eax,eax                            ;для выхода с eax==0
    20.     align 16                               ;на всякий случай :)
    21.     @Loop:
    22.         mov al,byte ptr[esi]
    23.         mov bl,byte ptr[edi]
    24.         test al,al                         ;если строка просмотрена до конца
    25.         jz @Ret                            ;выход с нулём в eax
    26.         ;------- кусок для обеспечения case insensitive ----------
    27.         ;.if al >= 'А' && al <='Я'
    28.         ;        add al,32
    29.         ;.elseif al >= 'A' && al <= 'Z'
    30.         ;        add al,32
    31.         ;.endif
    32.         ;.if bl >= 'А' && bl <= 'Я'
    33.         ;        add bl,32
    34.         ;.elseif bl >= 'A' && bl <= 'Z'
    35.         ;        add bl,32
    36.         ;.endif
    37.         ;---------------------------------------------------------
    38.         cmp al,bl                          ;поиск совпадения
    39.         je @Next                   
    40.         mov ecx,MatchLen                   ;если символы не совпадают, длина искомой цепочки=MatchLen
    41.         inc esi                            ;следующий байт строки
    42.         mov edi,MatchAddr                  ;Match-строку будем смотреть с начала
    43.         jmp @Loop
    44.         @Next:                             ;если символы совпали
    45.             dec ecx                        ;уменьшаем кол-во оставшихся в "очереди совпадения"
    46.             jnz @F                 
    47.                 sub esi,MatchLen           ;если "очередь совпадения" закончилась,
    48.                 inc esi                    ;вычисление позиции, с которой начинается
    49.                 mov eax,StrAddr       ;последовательность символов, совпадающая
    50.                 sub esi,eax                ;с MatchString
    51.                 inc esi
    52.                 xchg eax,esi               ;выход. В eax- позиция вхождения
    53.                 jmp @Ret
    54.             @@:
    55.             inc esi                        ;просмотр следующих байтов
    56.             inc edi
    57.             jmp @Loop  
    58.        
    59.  
    60. @InvalidStart:
    61.     mov eax,-1
    62.     ret    
    63. @OutOfString:
    64.     mov eax,-2 
    65. @Ret:  
    66.     ret
    67.  
    68. InStringI endp




    При сравнении с InString получается, что если искать с позиций в начале строки (Start=1), то моя процедура работает на 20-25% быстрее, если искать с позиции где-нибудь в конце строки, то уже InString быстрее на те же 20-25%.



    Можно ли ещё выжать скорости из этой процедуры, сам сколько смог, уже сделал?



    P.S.

    Закомментированный кусок, обеспечивающий регистронезависимость, можно пока не рассматривать.
     
  2. S_T_A_S_

    S_T_A_S_ New Member

    Публикаций:
    0
    Регистрация:
    27 окт 2003
    Сообщения:
    1.754
    Некоторые мысли: регистр edx не используется, можно хранить в нём, например, MatchLen.

    Переходы вперёд предсказываются как not taken, наиболее вероятные лучше делать назад.



    Немного подредактировал, в основном по размеру, не знаю будет ли работать =)
    Код (Text):
    1.     @Loop:
    2.         mov al,byte ptr[esi]
    3.         mov bl,byte ptr[edi]
    4.         test al,al                         ;если строка просмотрена до конца
    5.         jz @Ret                            ;выход с нулём в eax
    6.  
    7.         inc esi                            ;следующий байт строки
    8.         inc edi
    9.         dec ecx
    10.  
    11.         cmp al,bl                          ;поиск совпадения
    12.                
    13.         cmovne ecx,MatchLen                   ;если символы не совпадают, длина искомой цепочки=MatchLen
    14.         cmovne edi,MatchAddr                  ;Match-строку будем смотреть с начала
    15.  
    16.         test ecx,ecx
    17.         jnz @Loop
    18.  
    19.                 sub esi,MatchLen           ;если "очередь совпадения" закончилась,
    20.                 inc esi                    ;вычисление позиции, с которой начинается
    21.                 mov eax,StrAddr       ;последовательность символов, совпадающая
    22.                 sub esi,eax                ;с MatchString
    23.  
    24.                 xchg eax,esi               ;выход. В eax- позиция вхождения
    25.  @Ret: 
    26.     ret
    27.        
    28.  
    29. @InvalidStart:
    30.     stc                                    ; CF - признак ошибки
    31.     sbb eax,eax                            ; при анализе делаем:
    32.     jmp @Ret                               ; inc eax
    33. @OutOfString:                              ; jz InvalidStart
    34.     xor eax,eax                            ; js OutOfString
    35.     sub eax,2  
    36.     jmp @Ret




    IMHO основным тормозом тут будет кусок для обеспечения case insensitive, поскольку там куча Jcc. Может СтОит сделать табличное преобразование регистра ?
     
  3. cresta

    cresta Active Member

    Публикаций:
    0
    Регистрация:
    13 июн 2004
    Сообщения:
    2.257
    S_T_A_S_



    Практически такие же мысли, вот только с переходами старался сделать и вперед и назад как not taken. А чем различатся направление перехода? Если переход not taken?

    И избавился от пары jmp-ов. А вот mov edx,MatchLen дивидендов не принесло. Ну и ещё align16 потыкал в разных местах, экспериментально подобрал оптимальное.


    Код (Text):
    1.     align 16   
    2.     mov edx,MatchLen          ;кол-во символов, которое должно совпасть
    3.     xor eax,eax               ;для выхода с eax==0
    4.     dec Start
    5.     add esi,Start             ;абсолютный стартовый адрес
    6.     dec esi
    7.     @Loop1:
    8.     mov edi,MatchAddr         ;стартовый адрес MatchString
    9.     mov ecx,edx
    10.     @Loop:
    11.         inc esi
    12.         mov al,byte ptr[esi]
    13.         mov bl,byte ptr[edi]
    14.         test al,al            ;если строка просмотрена до конца
    15.         jz @Ret               ;выход с нулём в eax
    16.         ;------- кусок для обеспечения case insensitive ----------
    17.         cmp al,'A'                  ;eng
    18.         jl @F
    19.             cmp al,'Z'
    20.             jg @F
    21.                 add al,32
    22.         @@:
    23.         cmp al,'А'
    24.         jl @MM
    25.             cmp al,'Я'
    26.             jg @MM
    27.                 add al,32
    28.         @MM:
    29.        
    30.         cmp bl,'A'                  ;eng
    31.         jl @F
    32.             cmp bl,'Z'
    33.             jg @F
    34.                 add bl,32
    35.         @@:
    36.         cmp bl,'А'
    37.         jl @MM1
    38.             cmp bl,'Я'
    39.             jg @MM1
    40.                 add bl,32
    41.         @MM1:
    42.         ;---------------------------------------------------------
    43.         cmp al,bl             ;поиск совпадения
    44.         jne @Loop1                 
    45.         inc edi
    46.         dec ecx               ;уменьшаем кол-во оставшихся в "очереди совпадения"
    47.         jnz @Loop                  
    48.             sub esi,edx       ;если "очередь совпадения" закончилась,
    49.             inc esi           ;вычисление позиции, с которой начинается
    50.             mov eax,StrAddr   ;последовательность символов, совпадающая
    51.             sub esi,eax       ;с MatchString
    52.             inc esi
    53.             mov eax,esi       ;выход. В eax- позиция вхождения
    54.             ret
    55.  
    56.  
    57. @InvalidStart:
    58.     mov eax,-1
    59.     ret    
    60. @OutOfString:
    61.     mov eax,-2 
    62. @Ret:  
    63.     ret






    В итоге в наиболее типичной ситуации: case insensitive поиск в начале строки длина MatchLen = 10 почти уравнял скорость в сравнении с InString : 1050 тиков против 1030. В регистрозависимом варианте получается где-то в 2 раза быстрее.



    А что за табличное преобразование регистра? Что за зверь такой? Можно чуть подробней? А то регистронезависимость пожирает ровно половину времени:dntknw:
     
  4. cresta

    cresta Active Member

    Публикаций:
    0
    Регистрация:
    13 июн 2004
    Сообщения:
    2.257
    S_T_A_S_



    Доработал чуть напильником твой вариант, а то не хотел работать: вставил jne @Loop


    Код (Text):
    1.     cmovne ecx,MatchLen
    2.     cmovne edi,MatchAddr
    3.     jne @Loop
    4.     test ecx,ecx
    5.     jnz @Loop




    чуть медленнее моего: 670:580 в регистрозависимом варианте

    Описания cmovne у меня нет, но по смыслу догадался, так она похоже тяжеловатая на подъём. По-моему лучше делать mov и два разных места входа в цикл, как в предыдущем посте: один для ne: jne @Loop1 другой для nz: jnz @Loop
     
  5. S_T_A_S_

    S_T_A_S_ New Member

    Публикаций:
    0
    Регистрация:
    27 окт 2003
    Сообщения:
    1.754
    cresta >




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



    >




    Заполняешь массивчик 256 байт:
    Код (Text):
    1.  
    2.      xor eax, eax
    3. @@:  mov byte[char_map+eax],al
    4. ;     cmp al, "А"
    5. ;     jb  case_Ok
    6. ;     cmp al, "Я"
    7. ;     ja  case_Ok
    8. ;     add byte[char_map+eax],20h
    9. ;case_Ok:
    10.      inc al
    11.      jnz @b


    Теперь каждый элемент массива соответствует ASCII коду буквы.

    К элементам, соответствующим ЗАГЛАВНЫМ буквам прибавляешь 20h, например раскомментировав строчки выше.



    Использовать так:
    Код (Text):
    1.  
    2.     movzx eax,byte ptr[esi]
    3.     mov   al,byte ptr[char_map + eax]
    4.  




    >




    Я подозревал, что мой код не рабочий, как всегда =)

    Тока не пойму, как jne @Loop может повлиять...

    Если в результате cmp al,bl флаг Z установлен (AL == BL), то значение ECX командой cmovne изменено не будет. и дальнейший test ecx,ecx + jnz @Loop эквивалентен ветке je @Next + jnz @F. Иначе (AL != BL) в ECX загружается MatchLen и test ecx,ecx + jnz @Loop эквивалентен ветке с не выполненным je @Next.

    MatchLen же не может быть равно 0?



    Хотя щас твой вариант даже наверное лучше. Тут imho всё равно сильно не наоптимизируешь, я просто пытался уйти от множества условных переходов.





    Ещё, обрати внимание, как можно делать выход из функции - каждый ret - это несколько pop, leave и ret N. Можно съэкономить пару байт.

    xor + sub - это 5 байт, как и mov eax,-2. но помимо этого устанавливает флаг переноса. Eстановка -1 в EAX чуть короче.

    Ты же пишешь на асме, а не на HLL, поэтому по выходу из функции можно просто посмотреть флаги, а не сравнивать возвращаемое значение с -2 или -1 -- эконимим ещё немного времени и места.
     
  6. cresta

    cresta Active Member

    Публикаций:
    0
    Регистрация:
    13 июн 2004
    Сообщения:
    2.257
    S_T_A_S_



    По идее правильно, одного jnz @Loop должно было хватить, но я может намудрил чего, кучу вариантов пробовал, и забыл откуда и куда шёл :) Про выход с флагами, как индикаторами ошибок - это хорошо, принимается :)

    С предсказанием перехода тоже понятно, только надо посмотреть, в зависимости от String и MatchString, куда наиболее вероятны переходы.

    С таблицей так и делал, кучу байт. Первоначально использовал xlat, но что-то с ним тормозит:dntknw: Переделал на такую штуку (xlat разложил вручную):

    lea ebx,Table

    mov al,byte ptr[edi]

    mov dl,byte ptr[ebx+eax]

    mov al,byte ptr[esi]

    mov al,byte ptr[ebx+eax]

    Так получается заметно быстрее.



    Пока имею такое:


    Код (Text):
    1.     ;--------------------------------------------
    2.     mov ecx,Start              
    3.     mov esi,StrAddr            
    4.     @StartPos:                 
    5.     cmp byte ptr [esi+ecx-1],0
    6.     je @OutOfString
    7.     loopnz @StartPos               
    8.     ;--------------------------------------------
    9.     cmp Start,0
    10.     jle @InvalidStart
    11.     ;--------------------------------------------
    12.     lea ebx,Table
    13.     align 16   
    14.     xor eax,eax
    15.     dec Start
    16.     add esi,Start
    17.     dec esi
    18.     @Loop1:
    19.         mov ecx,MatchLen
    20.         mov edi,MatchAddr
    21.     @Loop:
    22.         inc esi
    23.         mov al,byte ptr[edi]
    24.         mov dl,byte ptr[ebx+eax]
    25.         mov al,byte ptr[esi]
    26.         mov al,byte ptr[ebx+eax]
    27.         test al,al
    28.         jz @Ret
    29.     ;---------------------------------------------------------
    30.             cmp al,dl
    31.             jne @Loop1                 
    32.         inc edi
    33.         dec ecx
    34.                 jnz @Loop
    35.                     sub esi,MatchLen
    36.                     inc esi
    37.                     mov eax,StrAddr
    38.                     sub esi,eax
    39.                     inc esi
    40.                     mov eax,esi
    41.                     ret
    42.     ;--------------------------------------------
    43. @InvalidStart:
    44.     mov eax,-1
    45.     ret    
    46. @OutOfString:
    47.     mov eax,-2 
    48. @Ret:  
    49.     ret
    50.     ;------- case insensitive Table ----------
    51.     Table:
    52.             db 0,1,2,3.....




    В принципе уже неплохо. При типичных для моего случаях String и MatchString работает быстрее, чем InString даже с case insensitive. Ну и флаги ещё привяжу.

    ОК, S_T_A_S_ спасибо за помощь :)
     
  7. S_T_A_S_

    S_T_A_S_ New Member

    Публикаций:
    0
    Регистрация:
    27 окт 2003
    Сообщения:
    1.754
    cresta



    Можно попробовать не занимать регистр ebx под адрес таблицы:



    mov dl,byte ptr[Table+eax]



    хотя врядли это чего прибавит, кроме размеру :)



    Только вместо:



    lea ebx,Table



    лучше делать:



    mov ebx,offset Table



    это меньше на байт (хотя пресловутый intel C compiler использует почему-то lea)
     
  8. alpet

    alpet Александр

    Публикаций:
    0
    Регистрация:
    21 сен 2004
    Сообщения:
    1.221
    Адрес:
    Russia
    Алгоритм для поиска подстрок с запоминанием смещений(в аттаче файл под tasm).

    Для ускорения размножил количество проверок первого символа образца, эдакий unroll. Это хорошо действует, когда сам символ встречается не очень часто. Думаю что поможет больший unroll (х32), поскольку можно будет делать префетч.

    Для безразличия к регистру символов, наверно лучше делать два сравнения, чем преобразования, типа:
    Код (Text):
    1.  
    2.   mov cl, 1
    3.   cmp [esi + 0], bl  ; cmp cc[0], 'a'
    4.   je  @cmpNextChar
    5.   cmp [esi + 0], bh  ; cmp cc[0], 'A'
    6.   je  @cmpNextChar
    7.   add cl, 1
    8.   cmp [esi + 1], bl  ; cmp cc[1], 'a'
    9.   ...  
    10.  




    [​IMG] _1792888953__scantxt.asm