Как получить доступ к массиву char и изменить строчные буквы в верхнем регистре, и наоборот
в настоящее время работает над проектом класса для структурированной компьютерной организации с использованием процессора x86. Значение, к которому я обращаюсь, составляет 1 байт char, но я не знаю, как сравнить его с прописным. Они сказали, что используют таблицу ascii в шестнадцатеричном формате, но я не уверен, как сравнить эти два.
void changeCase (char char_array[], int array_size ) {
__asm{
// BEGIN YOUR CODE HERE
mov eax, char_array; //eax is base image
mov edi, 0;
readArray:
cmp edi, array_size;
jge exit;
mov ebx, edi; //using ebx as offset
shl ebx, 2;
mov cl, [eax + ebx]; //using ecx to be the storage register
check:
//working on it
cmp cl, 0x41; //check if cl is <= than ASCII value 65 (A)
jl next_indx;
cmp cl, 0x7A; //check if cl is >= than ASCII value 122 (z)
jg next_indx;
cmp cl, 'a';
jl convert_down;
jge convert_up;
convert_down:
or cl, 0x20; //make it lowercase
jmp write;
convert_up:
and cl, 0x20; //make it uppercase
jmp write;
write:
mov byte ptr [eax + ebx], cl //slight funky town issue here,
next_indx:
inc edi;
exit:
cmp edi, array_size;
jl readArray;
mov char_array, eax;
// END YOUR CODE HERE
}
}
Что-то помогает на этом этапе. Заранее спасибо за помощь!
изменить 1:
Спасибо за все предложения и точки ясности, отредактировал мой код, чтобы отразить изменения. Некоторая проблема с нарушением прав доступа теперь.
изменить 2 (+):
Спасибо за полезные глаза людям. Я все еще собираюсь переводить все письма сейчас.
Ответы
Ответ 1
Для большей ясности я просто использую чистую сборку и предполагаю, что...
-
char_array
- это 32-разрядный указатель на [ebp+8]
.
-
array_size
- это 32-битное число с двумя дополнениями в [ebp+12]
.
- Для вашей платформы (так или иначе),
char
кодировка - ASCII.
Вы должны иметь возможность вывести это самостоятельно в встроенную сборку. Теперь, если вы посмотрите на таблицу, которую все должны помнить, но едва ли кто-нибудь делает, вы заметите некоторые важные детали...
- Верхние буквы
A
через Z
отображаются в коды 0x41
через 0x5A
соответственно.
- Строчные буквы
A
через Z
отображаются в коды 0x61
через 0x7A
соответственно.
- Все остальное не является буквой и, следовательно, не нуждается в преобразовании случаев.
- Если вы посмотрите на двоичное представление диапазонов букв верхнего и нижнего регистра, вы заметите, что они точно такие же, за исключением того, что заглавные буквы имеют бит 6, очищенный, а нижний регистр задан.
В результате алгоритм будет...
while array_size != 0
byte = *char_array
if byte >= 0x41 and byte <= 0x5A
*char_array |= 0x20 // Turn it lowercase
else if byte >= 0x61 and byte <= 0x7A
*char_array &= 0xDF // Turn it uppercase
array_size -= 1
char_array += 1
Теперь переведите это в сборку...
mov eax, [ebp+8] # char *eax = char_array
mov ecx, [ebp+12] # int ecx = array_size
.loop:
or ecx, ecx # Compare ecx against itself
jz .end_loop # If ecx (array_size) is zero, we're done
mov dl, [eax] # Otherwise, store the byte at *eax (*char_array) into `char dl`
cmp dl, 'A' # Compare dl (*char_array) against 'A' (lower bound of uppercase letters)
jb .continue # If dl` (*char_array) is lesser than `A`, continue the loop
cmp dl, 'Z' # Compare dl (*char_array) against 'Z' (upper bound of uppercase letters)
jbe .is_uppercase # If dl (*char_array) is lesser or equal to 'Z', then jump to .is_uppercase
cmp dl, 'a' # Compare dl (*char_array) against 'a' (lower bound of lowercase letters)
jb .continue # If dl (*char_array) is lesser than 'a', continue the loop
cmp dl, 'z' # Compare dl (*char_array) against 'z' (upper bound of lowercase letters)
jbe .is_lowercase # If dl (*char_array) is lesser or equal to 'z', then jump to .is_lowercase
jmp .continue # All tests failed, so continue the loop
.is_uppercase:
or dl, 20h # Set the 6th bit
mov [eax], dl # Send the byte back to where it came from
jmp .continue # Continue the loop
.is_lowercase:
and dl, DFh # Clear the 6th bit
mov [eax], dl # Send the byte back to where it came from
jmp .continue # Continue the loop
.continue:
inc eax # Increment `eax` (`char_array`), much of like a pointer increment
dec ecx # Decrement `ecx` (`array_size`), so as to match the previous pointer increment
jmp .loop # Continue
.end_loop:
Как только код достигнет .end_loop
, вы закончили.
Я надеюсь, что это свело на вас свет!
Ответ 2
в ASCII 'a' - 'z' и 'A' - 'Z' эквивалентны, кроме одного бита, 0x20
ваш друг здесь XOR.
если у вас есть char (либо "A" - "Z", либо "a" - "z" ), XORing с 0x20 будет переключать регистр;
до XORing, выполнение проверки диапазона имеет смысл. (чтобы увидеть, действительно ли значение действительно является буквой). Вы можете упростить эту проверку диапазона с помощью ORing значения, которое нужно проверить с помощью 0xef, которое сделает "a" - "A" и "z" - "Z", а затем сделайте проверку диапазона только один раз (если вы сравниваете только с < 'a' и > 'Z', вы будете пропускать символы между ними ('[', ']' и т.д.)
Ответ 3
Вариации этого вопроса задают все время. Эта версия проблемы (требующая условного поведения за пределами только if(isalpha(c)) c|=0x20;
)) сделала проблему достаточно сложной, чтобы не сразу было ясно, как это сделать эффективно.
Оказывается, что xor
не сложно было придумать, и для преобразования этого кода в безоговорочный режим ожидания или просто требуется простое изменение от xor 0x20
до and ~0x20
или or 0x20
. (Упрощение еще немного возможно.)
Вот как я сделал бы это с попыткой оптимально эффективного asm. Я даже включил версию с векторами SIMD и еще одну версию байтового цикла, используя идею отладки, которую я получил от векторизации.
Чтение этого ответа, вероятно, полезно только после того, как вы поймете основные принципы, связанные с его решением, с не оптимизированным кодом. OTOH, на самом деле очень мало операций, поэтому нет большого количества кода для поиска. И я очень прокомментировал это. Есть много полезных ссылок в x86 wiki, начиная с учебников и заканчивая справочными руководствами по настройке производительности.
Преобразование между нижним и верхним регистром алфавитных символов ASCII требует только установки или очистки бита 0x20
, поскольку набор символов ASCII выложен диапазонами 32 друг от друга и не пересекает границу mod32.
Для каждого байта:
- сделать копию и безоговорочно ИЛИ с помощью 0x20
- проверьте, не существует ли между
'a'
и 'z'
- если это так, переверните буквенный бит ASCII с использованием
xor
и сохраните результат обратно в массив.
Выполнение теста ASCII isalpha(3)
таким образом безопасно: только исходные байты, которые попадают в диапазон 'a'
.. 'z'
, от установки этого бита - это буквенные символы в верхнем регистре. Это просто математика, которая работает для любых двух диапазонов равного размера, которые не пересекают границу %32
. (Или a %64
, если соответствующий бит был 0x40
, например).
Чтобы сделать сравнение еще эффективнее, я использую трюк без знака, поэтому внутри цикла используется только одна условная ветвь (кроме самого условия цикла). См. Комментарии в коде для объяснения.
/******** Untested. ************/
// ASCII characters are flipped to the opposite case (upper <-> lower)
// non-ASCII characters are left unchanged
void changeCase (char char_array[], int array_size ) {
__asm{
// BEGIN YOUR CODE HERE
mov esi, char_array; // MSVC inline asm requires these potentially-redundant copies :(
mov ecx, array_size;
test ecx,ecx; // return if(size <= 0)
jle early_out;
next_char:
mov al, [esi]; // load the current character
mov dl, al;
// check if the character is alphabetic or not
// there are two equal-size ranges of characters: one with 0x20 set, and one without
or al, 0x20; // set 0x20 and then just check that lowercase range
// unsigned compare trick: 0 <= n < high can be done with one unsigned compare instead of two signed compares
// low < n < high can be done by shifting the range first
sub al, 'a'; // if al is less than 'a', it will become a large unsigned number
cmp al, 'z'-'a';
ja non_alpha; // conditionally skip the flip & store
xor dl, 0x20; // toggle the ASCII case bit
mov [esi], dl;
// xor [esi], 0x20 // saves the mov earlier, but is otherwise slower
non_alpha:
inc esi;
dec ecx;
jz next_char;
early_out:
// END YOUR CODE HERE
}
}
Этот код может быть более читаемым, если часть материала "design doc" находится в блоке вне кода. Это сильно забивает вещи, и похоже, что там много кода, но на самом деле очень мало инструкций. (Их просто сложно объяснить короткими комментариями. Комментирующий код сложный: комментарии, которые слишком очевидны, просто беспорядочны и требуют времени от чтения кода и полезных комментариев.)
Векторизованное
На самом деле для x86 я бы использовал SSE или AVX для выполнения 16B за один раз, выполняя тот же алгоритм, но делая сравнения с двумя pcmpgtb
. И, конечно, безоговорочно сохраняя результаты, поэтому массив всех неалфавитных символов по-прежнему будет загрязнен в кеше, используя большую пропускную способность памяти.
Нет никакого сопоставления SSE без знака, но мы можем по-прежнему изменять диапазон, который мы ищем вниз. Нет значений меньше -128
, поэтому в подписанном сравнении он работает так, как 0
делает в беззнаковом сравнении.
Чтобы сделать это, вычтите 128
. (или добавить, или xor (безличный add), где некуда/несут/нет). Это можно сделать в той же операции, что и вычитание 'a'
.
Затем используйте результат сравнения как маску для обнуления байтов в векторе 0x20
, поэтому только буквенные символы получают XORed с 0x20. (0 - единичный элемент для XOR/add/sub, что часто очень удобно для условных обозначений SIMD).
См. также версию strtoupper
, которая была протестирована, и код для вызова в цикле, включая обработку не- несколько-16 входов, на строках C с неявной длиной (поиск по завершению 0 на лету).
#include <immintrin.h>
// Call this function in a loop, with scalar cleanup. (Not implemented, since it the same as any other vector loop.)
// Flip the case of all alphabetic ASCII bytes in src
__m128i inline flipcase(__m128i src) {
// subtract 'a'+128, so the alphabetic characters range from -128 to -128+25 (-128+'z'-'a')
// note that adding 128 and subtracting 128 are the same thing for 8bit integers.
// There nowhere for the carry to go, so it just xor (carryless add), flipping the high bit
__m128i lcase = _mm_or_si128(src, _mm_set1_epi8(0x20));
__m128i rangeshift= _mm_sub_epi8(lcase, _mm_set1_epi8('a'+128));
__m128i non_alpha = _mm_cmpgt_epi8(rangeshift, _mm_set1_epi8(-128 + 25)); // 0:alphabetic -1:non-alphabetic
__m128i flip = _mm_andnot_si128(non_alpha, _mm_set1_epi8(0x20)); // 0x20:alpha 0:non-alpha
return _mm_xor_si128(src, flip);
// just mask the XOR-mask so non-alphabetic elements are XORed with 0 instead of 0x20
// XOR identity value is 0, same as for addition
}
Этот компилируется в хороший код, даже без AVX, только с одним дополнительным movdqa
, чтобы сохранить копию регистра. См. Ссылку godbolt для двух более ранних версий (один из которых использует два сравнения, чтобы сохранить их простыми, а другой - с помощью pblendvb
, прежде чем я вспомнил о том, чтобы замаскировать вектор 0x20
вместо результата.)
flipcase:
movdqa xmm2, XMMWORD PTR .LC0[rip] ; 0x20
movdqa xmm1, xmm0
por xmm1, xmm2
psubb xmm1, XMMWORD PTR .LC1[rip] ; -31
pcmpgtb xmm1, XMMWORD PTR .LC2[rip] ; -103
pandn xmm1, xmm2
pxor xmm0, xmm1
ret
section .rodata
.LC0: times 16 db 32
.LC1: times 16 db -31
.LC2: times 16 db -103
Эта же идея использования нераспределенного теста также будет работать для цикла байтов:
mov esi, char_array;
mov ecx, array_size;
test ecx,ecx; // return if(size <= 0)
jle .early_out;
ALIGN 16 ; really only need align 8 here, since the next 4 instructions are all 2 bytes each (because op al, imm8 insns have a special encoding)
.next_char:
mov al, [esi]; // load the current character
mov dl, al;
// check if the character is alphabetic or not
or al, 0x20;
sub al, 'a';
cmp al, 'z'-'a'; // unsigned compare trick: 'a' <= al <= 'z'
setna al; // 0:non-alpha 1:alpha (not above)
shl al, 5; // 0:non-alpha 0x20:alpha
xor dl, al; // conditionally toggle the ASCII case bit
mov [esi], dl; // unconditionally store
inc esi;
dec ecx; // for AMD CPUs, or older Intel, it would be better to compare esi against an end pointer, since cmp/jz can fuse but dec can't. This saves an add ecx, esi outside the loop
jz .next_char;
.early_out:
Для 64-битного кода используйте rsi
вместо esi
. Все остальное одно и то же.
По-видимому MSVC inline asm не позволяет имена .label
локальных символов. Я изменил их для первой версии (с условной ветвью), но не с этим.
Использование movzx eax, byte [esi]
может быть немного лучше на некоторых процессорах, чтобы избежать ложной зависимости от значения eax при вводе функции. OTOH, только AMD имеет эту проблему (и Silvermont), но movzx
не так дешев, как нагрузка на AMD. (Это на Intel, один uop, который использует только порт загрузки, а не порт ALU). Работает на al
после того, как это все еще хорошо, поскольку оно позволяет избежать частичного регистрационного стенда (или дополнительных инструкций, чтобы избежать его) от чтения eax
после setcc
записывает al
. (Нет setcc r/m32
, только r/m8
, к сожалению).
Мне нужно задаться вопросом, что думал бы профессор, если бы кто-то передал код, подобный этому, для такого задания.: P Я сомневаюсь, что даже интеллектуальный компилятор будет использовать трюк setcc
/shift
, если вы не приведете к нему компилятор. (Может быть, unsigned mask = (tmp>='a' && tmp<='z'); mask <<= 5; a[i] ^= mask;
или что-то в этом роде.) Компиляторы знают о трюке без знака, но gcc не использует его в некоторых случаях для компиляции, проверки диапазона по времени, даже если это может доказать, что диапазон достаточно мал.
Ответ 4
Предоставлено @KemyLand для полезного разбиения кода сборки, я выяснил, как преобразовать верхний регистр в нижний регистр и наоборот.
void changeCase (char char_array[], int array_size ) {
//this function is designed to change lowercase letters to uppercase, and vice-versa, from a char-array given the array and its size.
__asm{
// BEGIN YOUR CODE HERE
mov eax, [ebp + 8]; //move to register value parameter 1 (the array)
mov ecx, [ebp + 12]; //likewise parameter 2 (the array size)
START:
or ecx, ecx; //check if pointer is 0
cmp ecx, 0;
je endloop; //go to end loop
mov dl,byte ptr [eax]; //not sure if needed, but reassurance
cmp dl, 0x41; // is char an A?
jl cont;
cmp dl, 0x5A; // is char a Z?
jle convertUP;
cmp dl, 0x61; // is char an a?
jl cont;
cmp dl, 0x7A; // is char a z?
jle convertDOWN;
jmp cont;
convertUP:
or dl, 0x20; //Yes! Finally got it working!
mov byte ptr [eax], dl;
jmp cont;
convertDOWN:
and dl, 0xdf; //this will work for sure.
mov[eax], dl;
jmp cont
cont:
inc eax;
dec ecx;
jmp START;
endloop:
}
}
Не стесняйтесь объяснять, что я мог пропустить! Спасибо всем, что помогли мне лучше понять процессор сборки x86.
Ответ 5
В таблице ascii все буквы непрерывны:
A=0x41=01000001
a=0x61=01100001
Z=0x5A=01011010
z=0x7A=01111010
Итак, вы можете видеть, что, переключая 6-й бит, вы преобразовываете форму сверху в нижний регистр.