Имитация конструкции switch..case

Последнее обновление: 02.07.2023

В ряде языков высокого уровня есть конструкция switch..case, которая последовательно сравнивает некоторое выражение с набором значений:

switch(выражение){
    case значение_1: действия_1;
    case значение_2: действия_2;
    case значение_3: действия_3;
    default: действия_по_умолчанию;
}

Если найдено совпадение, то выполняются инструкции из соотвествующего блока case

Общий алгоритм данной конструкции может быть следующим:

  1. С помощью инструкции CMP сравниваем значение регистра с некоторым значением.

  2. Если сравнение оказалось НЕ верным, то переходим к следующей метке, которая сравнивает значение регистра с новым значением.

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

  3. Если ни одно из сравнений в инструкциях CMP НЕ является верным, выполняем действия по умолчанию.

Пример реализации:

.data
    number dword 2
.code
main proc
    
    mov eax, number ; сравниваемое значение
    cmp eax, 1      ; number == 1
    jne case_2      ; если не равно, переходим к метке case_2
    mov eax, 11     ; действия, если числа равны
    jmp end_switch  ; выходим из конструкции
case_2:
    cmp eax, 2      ; number == 2
    jne case_2      ; если не равно, переходим к метке case_3
    mov eax, 22     ; действия, если числа равны
    jmp end_switch  ; выходим из конструкции
case_3:
    cmp eax, 3      ; number == 3
    jne default      ; если не равно, переходим к мет
    mov eax, 33     ; действия, если числа равны
    jmp end_switch  ; выходим из конструкции
default:
    mov eax, 44     ; действия по умолчанию, если предыдущие равенства не верны
end_switch:         ; конец конструкции
    ret
main endp
end

Здесь значение переменной number последовательно сравнивается с тремя значениями, и в зависимости от результатов сравнения в регистр eax помещается то или иное значение.

Поскольку достижение последней метки требует большего времени, то сравнения с наиболее вероятными значениями следует помещать ближе к началу конструкции.

Оптимизация

Приведенный выше пример довольно просто, однако имеет один большой недостаток - при большом количестве меток потребуется некоторое время чтобы дойти до последней метки. Например, если число равно 10, то будет выполняться кода на метке default. Но чтобы дойти до нее надо пройти все предыдущие метки и инструкции сравнения. При большом количестве переходов и меток это несколько снижает производительность. И здесь есть различные возможности оптимизации программы.

Если сравниваемые значения располагаются последовательно, как примере выше - 1, 2, 3, то эффективнее определить набор меток, к которым происходит переход при помощи косвенной адресации:

.data
    number dword 2
.code
main proc
    
    mov eax, number ; сравниваемое значение
    cmp eax, 1
    jb default      ; если меньше 1
    cmp eax, 3
    ja default      ; если больше 3
    
    lea rcx, labels ; загружаем все метки
    jmp qword ptr [rcx][rax * 8 - 8]    ; переходим к одной из меток
labels qword case_1, case_2, case_3     ; адреса меток
case_1:
    mov eax, 11     ; действия, если числа равны
    jmp end_switch  ; выходим из конструкции
case_2:
    mov eax, 22     ; действия, если числа равны
    jmp end_switch  ; выходим из конструкции
case_3:
    mov eax, 33     ; действия, если числа равны
    jmp end_switch  ; выходим из конструкции
default:
    mov eax, 44     ; действия по умолчанию, если предыдущие равенства не верны
end_switch:         ; конец конструкции
    ret
main endp
end

Здесь опять же сравниваем с теми же числами - 1, 2, 3. Вначале проверяем попадает ли число в этот диапазон - если оно меньше 1 и больше 3, то переходим к метке default. Для остальных переходов определяем специальную переменную - labels, которая хранит адреса трех меток. Важно, что эта переменная определена в таком месте процедуры, в где она точно не будет выполняться как инструкция - в данном случае перед переменной идет переход к одной из меток.

Адрес этой переменной загружается в регистр RCX. И далее на основе значения в регистре EAX вычисляем адрес и переходим к нему:

jmp qword ptr [rcx][rax * 8 - 8]

То есть [rcx] представляет адрес переменной labels. Адрес первой метки это также [rcx] или [rcx] + 0. В RAX мы ожидаем число 1, 2 или 3. Соответственно с помощью выражения [rax * 8 - 8] мы можем вычислить смещение метки для определенного числа в EAX. Например, если в EAX число 2, то мы получим [rcx][2*8-8] или [rcx][8], то есть адрес метки case_2.

Но что, если нам надо сравнить с числами, которые идут не попорядку, например, 1, 2, 4, 8? Здесь между числами небольшой разбор - от 1 до 8, и мы могли бы сделать так:

.data
    number dword 2
.code
main proc
    
    mov eax, number ; сравниваемое значение
    cmp eax, 1
    jb default      ; если меньше 1
    cmp eax, 8
    ja default      ; если больше 8
    
    lea rcx, labels ; загружаем все метки
    jmp qword ptr [rcx][rax * 8 - 8]

labels qword case_1, case_2, default, case_4, 
             default, default, default, case_8    ; адреса меток
case_1:
    mov eax, 12     
    jmp end_switch  
case_2:
    mov eax, 23     
    jmp end_switch  
case_4:
    mov eax, 45 
    jmp end_switch
case_8:
    mov eax, 89 
    jmp end_switch
default:
    mov eax, 101    ; действия по умолчанию, если предыдущие равенства не верны
end_switch:         ; конец конструкции
    ret
main endp
end

Здесь все то же самое, только теперь адресов меток у нас 8, но 4 из них - адреса метки default. То есть если у нас будут для сравнения числа 3, 5, 6, 7, то для них будет выбираться метка default.

Хотя в примере выше сравниваемые значение не идут по порядку, но все равно находятся довольно близко - в пределах первой восьмерки. Что делать, если разброс значений гораздо больше, например, 1, 2, 10, 11, 100, 101, 103, 1000, 1001? В этом случае мы можем сгруппировать близкие значения в отдельные группы и для каждой групп определить свой набор меток:

.data
    number dword 2
.code
main proc
    
    mov eax, number ; сравниваемое значение

    cmp eax, 100
    jb case0_11
    cmp eax, 103
    ja case1000_1001
    lea rcx, labels100
    jmp qword ptr [rcx][rax * 8 - 100 * 8]
    labels100 qword case_100, case_101, default, case_103  ; метки для 100, 101, 103

case0_11:
    cmp eax, 11
    ja default          ; если больше 10
    lea rcx, labels0_11
    jmp qword ptr [rcx][rax * 8]
    labels0_11 qword default, case_1, case_2, default, default, default, default,
             default, default, default, case_10, case_11

case1000_1001:
    cmp eax, 1000
    jb default
    cmp eax, 1001
    ja default
    lea rcx, labels1000
    jmp qword ptr [rcx][rax * 8 - 1000 * 8]
    labels1000 qword case_1000, case_1001

case_1:
    mov eax, 21    
    jmp end_switch 

case_2:
    mov eax, 22   
    jmp end_switch 

case_10:
    mov eax, 23  
    jmp end_switch

case_11:
    mov eax, 24
    jmp end_switch 

case_100:
    mov eax, 25
    jmp end_switch 

case_101:
    mov eax, 26
    jmp end_switch 

case_103:
    mov eax, 26
    jmp end_switch 


case_1000:
    mov eax, 27
    jmp end_switch 


case_1001:
    mov eax, 28
    jmp end_switch 

default:
    mov eax, 30    ; действия по умолчанию, если предыдущие равенства не верны
end_switch:         ; конец конструкции
    ret
main endp
end

Здесь все сравниваемые значения разбиты на три группы, для каждой из которых определен свой набор адресов меток. Данный способ в какой-то степени можно назвать бинарным поиском - среди групп находим группу со срединными значениями и остальные группы разбиваем на две части - одна со значениями меньше середины, а другая - со значениями больше середины.

С одной стороны, это ведет к усложнению программы и снижению ее читабельности, а с другой, увеличению скорости при большом количестве меток.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850