Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Литература часть 2 по АЭВМ / Методические указания / Методичка (есть пара багов).doc
Скачиваний:
22
Добавлен:
28.03.2016
Размер:
351.23 Кб
Скачать

Контрольные вопросы и задания

1. Если в C++ не отключено декорирование имён, то какое имя будет помещено в объектный файл для функции int func(int x, double y) ?

2. В ассемблерных вставках нельзя использовать директиву OFFSET. А как ещё можно загрузить в регистр адрес переменной?

3. Каким образом процедура на ассемблере должна возвращать значение, чтобы из C++ её можно было вызывать как функцию?

4. Пусть в ассемблерном модуле стоит директива .model flat, stdcall, и вы не хотите её изменять. Каким образом вызывать процедуры этого модуля из C++?

5. Каким образом в используемом вами компиляторе C++ сгенерировать ассемблерный листинг результата компиляции программы?

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

Задания по программированию

1. Решите на языке С++ задачу №37 из проверяющей системы (в ней требуется подсчитать количество простых чисел в диапазоне от 1 до n, где n не превышает 10 млн.) Ввод и вывод сделайте средствами C++, а само вычисление простых чисел выполните с помощью ассемблерной вставки. В качестве алгоритма рекомендуется использовать решето Эратосфена (изучалось в курсе «структуры и алгоритмы обработки данных»). Постарайтесь максимально эффективно реализовать данный участок кода.

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

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

Затем напишите на языке C++ функцию сравнения CmpFunc, которой передаётся два целых числа и она возвращает -1, если первое число меньше второго, 0, если они равны и +1, если первое число больше второго.

В функции main заполните массив случайными числами и вызовите процедуру BubbleSort, передав ей адрес массива, число элементов и адрес функции CmpFunc. Отсортированный массив выведите на консоль.

Практическая работа 7. Представление вещественных чисел в памяти эвм, работа с математическим сопроцессором Краткие теоретические сведения и примеры

1. Перевод в двоичную систему и обратно. Как и целые, вещественные числа в памяти ЭВМ хранятся также в двоичной системе. Например, число 2.25 в двоичной системе выглядит как 10.01. Прежде чем изучать особенности машинного представления, рассмотрим перевод вещественных чисел в двоичную систему и обратно.

Целую часть числа мы переводить умеем (см. практ. работу 1), поэтому будем рассматривать лишь дробную часть. Проще всего перевести из двоичной системы в десятичную – нужно сложить те степени двойки, для которых в дробной части числа стоит единица. Например, 0.01001 = 2-2+2-5=0.28125

Для перевода из десятичной системы в двоичную рассмотрим два способа. Первый вариант – перебирать по очереди степени двойки - 2-1, 2-2, 2-3 и т.д. Каждую такую степень сравниваем с нашим числом. Если она больще, в ответ пишем ноль. Если меньше или равна, в ответ пишем 1 и вычитаем из нашего числа.

Для примера выполним перевод 0.59375. Вначале пробуем степень 2-1=0.5. Она не превышает числа, поэтому в ответ пишем 1 и вычитаем – остаётся 0.59375-0.5 = 0.09375. Теперь пробуем 2-2=0.25 – она больше 0.09375, поэтому в ответ просто пишем ноль. Следующая степень 2-3=0,125 получилась снова больше нашего числа - в ответ снова идёт ноль. Далее пробуем 2-4=0.0625 – она подходит, в ответ идёт 1, после вычитания остаётся 0,03125. Наконец, следующая степень 2-5 равна как раз 0,03125, поэтому в ответ идёт 1 и вычисление завершено. Ответ - 0.10011.

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

Если при переводе в двоичную систему получается число бесконечной длины, и мы хотим найти период повторения цифр в дробной части, то можно использовать второй способ перевода [2]. Исходное число записывается в виде простой дроби, числитель и знаменатель переводятся в двоичную систему и выполняется деление столбиком. Если в процессе деления получим делимое, которое уже встречалось ранее, то период найден.

Для примера возьмём число 0.3. Его простая дробь выглядит как 3/10 или в двоичной системе как 11/1010. Производим деление столбиком:

11|1010

---------

0.01001

_1100

1010

_10000

1010

1100

Видим, что делимое 1100 повторилось – значит, дальше в ответе цифры тоже начнут повторяться. Период получился равным 1001, итоговое число можно записать как 0.0(1001), где период обозначается круглыми скобками.

Примечание. Возможно, в 21-м веке уже не все студенты помнят, как выполняется деление в столбик (в связи с повсеместным использованием калькуляторов и компьютеров). Данный материал выходит за рамки методических указаний, но его легко найти в сети Internet.

Одно и то же вещественное число можно записать по-разному. Например, 15.73 = 1.573*10 = 0.1573*102 = 1573*10-2. Вариант записи, в которой перед десятичной точкой стоит ровно одна ненулевая цифра, называется нормализованным представлением, а его получение - нормализацией. Для числа 15.73 нормализованным представлением будет 1.573 * 101, где 1.573 – мантисса, 1 – показатель степени (иногда еще его называют порядок).

Для двоичных чисел нормализация выполняется аналогично – например, число 1011.01101 после нормализации выглядит как 1.01101101 * 23. В памяти ЭВМ вещественные числа хранятся в основном в нормализованном виде (за некоторым исключением).

2. Представление в памяти ЭВМ. Рассмотрим теперь представление вещественных чисел в памяти ЭВМ [2]. В стандарте IEEE 754-1985 описаны 3 формата, их описание приведено в следующей таблице.

Таблица 3. Форматы вещественных чисел

Формат

Описание

С одинарной точностью

(в masm - REAL4, в С++ - float)

Длина 32 бита, из них слева направо: 1 бит – знак, 8 битов – показатель степени, 23 бита – дробная часть мантиссы. Диапазон примерно от 2-126 до 2127 (и отрицательные)

С двойной точностью (в masm - REAL8, в C++ - double)

Длина 64 бита, из них слева направо: 1 бит – знак, 11 битов – показатель степени, 52 бита – дробная часть мантиссы. Диапазон примерно от 2-1022 до 21023 (и отрицательные)

Расширенное с двойной точностью (в masm - REAL10, в C++ - long double)

Длина 80 бит, из них слева направо: 1 бит – знак, 16 битов – показатель степени, 63 бита – дробная часть мантиссы. Диапазон примерно от 2-16382 до 216383 (и отрицательные)

Рассмотрим отдельно каждую часть представления числа. Бит знака равен 1, если число отрицательное, и 0, если положительное.

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

В поле показателя степени хранится показатель в виде 8-разрядного беззнакового целого числа, к которому прибавлено 127. Например, для числа 1.101*25 значение показателя равно 5+127=132, или в двоичном виде 10000100.

Для примера найдём представление с одинарной точностью числа 1101.101. Вначале выполним его нормализацию – получим 1.101101*23. Дробная часть мантиссы равна 101101, справа к ней допишем нули так, чтобы получилось 23 бита: 10110100000000000000000. В поле показателя степени будет 3+127=130, в двоичном виде это 10000010. Бит нуля равен 0. Итого получилось 01000001010110100000000000000000, или в более коротком шестнадцатеричном виде 415A0000.

Заметим, что не все вещественные числа можно представить в памяти ЭВМ в нормализованном виде. Если число очень близко к нулю, то в нормализованном варианте показатель степени может быть настолько большим, что не влезет в отведенное поле. Возьмём, например, число 101010*2127. В нормализованном виде оно превратится в 1.01010*2132, то есть в поле показателя степени должно быть 132+127=259. В значение с одинарной точностью такой показатель степени уже не поместится. Если в результате вычислений получилось такое число, оно будет сохранено в денормализованном виде, а в сопроцессоре возникнет ситуация потери точности (underflow). Особенности работы сопроцессора с такими числами выходят за рамки данных методических указаний.

В стандарте IEEE754-1985 представления вещественных чисел кроме собственно чисел, предусмотрено ещё несколько специальных значений [2] – они приведены в следующей таблице.

Таблица 4. Специальные значения в двоичных вещественных числах одинарной точности

Значение

Знак, показатель, мантисса

Положительный ноль

0 00000000 00000000000000000000000

Отрицательный ноль

1 00000000 00000000000000000000000

Положительная бесконечность

0 11111111 00000000000000000000000

Отрицательная бесконечность

1 11111111 00000000000000000000000

Тихое NaN (QNaN)

x 11111111 1xxxxxxxxxxxxxxxxxxxxxx

Громкое NaN (SNaN)

x 11111111 0xxxxxxxxxxxxxxxxxxxxxx

В таблице под символом x понимается любое значение (0 или 1). Для SNaN поле мантиссы должно содержать хотя бы одну единицу.

Значения бесконечностей можно сравнивать между собой и с другими числами, но при выполнении операций с ними может возникнуть переполнение (overflow).

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

3. Работа с FPU. Рассмотрим теперь кратко применение сопроцессора для вычислений с вещественными числами.

Примечание. Вообще, название «сопроцессор» возникло вследствие того, что вначале это было действительно отдельное аппаратное устройство. В настоящее время он встраивается в центральный процессор, и всё чаще используется термин FPU (floating point unit – модуль вычисления с плавающей точкой).

В состав FPU входят 8 80-разрядных регистров R0-R7. Эти регистры организованы в виде стека, поэтому напрямую к ним обращаются редко – обычно используют относительные имена ST(0) – ST(7) или вообще применяют команды, не требующих указания регистров. Здесь ST(0) – вершина стека (один из 8 регистров), ST(1) – элемент в стеке перед ним и т.д.

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

Рассмотрим команды, которые нам потребуются. Fld – поместить в стек вещественное число из памяти, fild – поместить в стек целое число из памяти (преобразовав в вещественное), fst – скопировать число с вершины стека в память, fstp - скопировать число с вершины стека в память и удалить из стека, fist - скопировать число с вершины стека в память в целочисленную ячейку, округлив до целого. Арифметические команды: fadd – сложение, fsub – вычитание, fdiv – деление, fmul - умножение и др.

Приведём пример вычисления суммы двух вещественных чисел.

include masm32rt.inc

.data

out_format BYTE "%f", 0

var_a REAL8 1.5

var_b REAL8 2.3

sum REAL8 ?

.code

main PROC

fld var_a ;поместить в стек

fadd var_b ;сложить

fst sum ;результат с вершины стека – в память

invoke crt_printf, ADDR out_format, sum

exit

main ENDP

end main

Рассмотрим теперь более сложный пример. Пусть надо вычислить значение выражения 3.1*(2.8+3.2). Сложное выражение удобно вначале перевести в обратную польскую (постфиксную) запись, где знак операции стоит не между аргументами, а после них (эта тема изучалась в курсе «Структуры и алгоритмы обработки данных»). Для данного примера получится 3.1 2.8 3.2 + *.

Затем идём слева направо по этому выражению и если встретим аргумент, то поместим его в стек, а если встретим операцию, то выполним её. Пример программы, где использовался данный подход:

include masm32rt.inc

.data

out_format BYTE "%f", 0

args REAL8 3.1, 2.8, 3.2

res REAL8 ?

.code

main PROC

fld args[0] ;аргумент в стек

fld args[1*TYPE args] ;аргумент в стек

fld args[2*TYPE args] ;аргумент в стек

fadd ;первая операция

fmul ; вторая операция

fst res ;результат из стека в память

invoke crt_printf, ADDR out_format, res

exit

main ENDP

end main

Рассмотрим теперь, как выполняется сравнение чисел. Для этого можно использовать команду fcom с одним аргументом – она вычитает из st(0) значение аргумента в памяти и выставляет соответствующие флаги FPU. Далее с помощью команды fstsw ax программа переписывает содержимое регистра состояния сопроцессора в регистр ax центрального процессора. Затем содержимое регистра ah переписывается в регистр флагов центрального процессора при помощи команды sahf. Биты кодов условий сопроцессора отображаются на регистр флагов центрального процессора таким образом, что для анализа кодов условий можно пользоваться командами условных переходов для беззнаковых целых чисел – ja, jb, jae и т.п. Пример кода:

fld varA ;загружаем в стек переменную varA

fcom varB ;сравниваем с переменной varB

fstsw ax ;переписываем флаги FPU в ax

sahf ;переписываем флаги из ah во flags

jb L1 ;если меньше, идём на метку

FPU поддерживает довольно большое число и других команд – например, fsqrt – вычисление квадратного корня, fsin – вычисление синуса, fabs – вычисление модуля числа. Полный перечень команд можно посмотреть в технической документации (например, на сайте www.intel.com), более подробные примеры – в учебниках из списка литературы.