Показать сообщение отдельно
Старый 18.02.2008, 22:20   #2
Пугатель
 
Аватар для [CCCP] Monster

 
Регистрация: 26.06.2005
Адрес: Москва, СССР
Сообщений: 6,102
Репутация: 1085 [+/-]
1. Подводные камни преобразования типов.

Некоторые соображения по поводу преобразования типов. Обычно учителя говорят, что прямым преобразованием типов пользоваться не следует - ибо это медленно и не круто. Однако, это не всегда медленно.

По сути своей, тип данных - это инструкция для компилятора, как трактовать массив байтов, находящийся в памяти по заданному адресу.

И из-за этого начинаются пляски с бубнами. Потому что есть такие типы, как строка, целое числои с плавающей почкой.

Строка в памяти хранится, как массив байт, где один байт кодирует один символ (в случае с ASCII, если это UNICODE, тогда 2 байта). Т.е. если, например, строка хранит число "10000", то она будет состоять из 5 байт, каждый из которых кодирует свой символ, и плюс еще один, нулевой символ в конце строки, показывающий, что строка кончилась и дальше значащих символов нет. Чтобы сконвертировать эту строку в целое, надо провести серию операций, которые реализуются стандартной библиотечной функцией atoi. Естественно, что такая конвертация отнимает время. Именно это и происходит, когда вы пишете:

Код:
int a;
AnsiString b = "10000";

a = (int)b;
Теперь число типа float. Дело в том, что дробные числа - очень важная штука в математике, и чтобы их считать, придумали целый формат данных, а в АЛУ процессоров x86 был внедрен математический сопроцессор, который занимается обработкой таких чисел. А представлены они в памяти в формате IEEE, который бывает 32, 64 и 80 разрядным. Сейчас правда еще и 128 разрядный есть. Суть там в том, что биты слева направо в числе задают свойства числа, т.е. (слева направо) первый бит - знак степени (0 - "+", 1 - "-"), далее 7 бит - степень (0-128), потом еще бит - знак самого числа, и все оставшиеся биты - это само число.

Подробнее см. в описании процессоров интел и ассемблера, нет смысла сейчас все это подробно расписывать, разговор-то не об этом.

Так вот, чтобы, например, провести такую операцию:

Код:
int a;
float b = 10.0f;
a = b;
Процессору необходимо перевести число из формата IEEE в формат целого 32-разрядного числа, и только после этого выполнить присваивание (команду MOV). Поэтому такой код тоже выполняется с задержками.

Есть еще один нюанс. Бывает, что вам необходимо считать из памяти по заданному адресу какие-то данные, которые, составляют, например, целое 32 разрядное число.


Код:
unsigned char array[8];
int count;

count = *((int*)(&array[1]));  //Случай А
count = *((int*)(&array[0]));  //Случай Б
На первый взгляд, случаи ничем не отличаются и должны работать одинаково. Мы всего лишь указали компилятору, что адрес нужного байта в массиве – это на самом деле указатель на целое число и присвоили значение этого целого переменной count. По идее, обычный зашколенный высокоуровневый программер должен сейчас сломать себе мозг, сказать что код работать не будет, либо, если он продвинутый и опытный дядька, сказать, что код будет работать одинаково быстро. Но на самом деле все далеко не так однозначно.

Руководство по процессорам Интел имеет информацию о такой вещи, как выравнивание данных в памяти по 4 байта. Это значит, что данные, читаемы процессором из памяти должны иметь указатель, кратный 4-м. Если это не так, срабатывает прерывание по ошибке. Однако ОС отслеживает такие указатели и генерирует «на лету» исполняемый код потока так, чтобы исключить этот конфликт и заставить систему работать правильно. В самом лучшем случае это делается путем замены одной команды MOV двумя, с разными адресами источников данных, так, чтобы считать только то, что нужно. Компилятор – тоже не дурак, и старается данные выравнивать, чтобы избежать нежелательных маневров с указателями, которые проводит ОС. Но вот в случае, приведенном выше, никакие маневры не помогут – ну память просто не удается выровнять.

И вот тут как раз и начинается интересное. Именно из-за выравнивания в памяти, код в случае А работает вдвое медленнее, чем код в случае Б.

Однако, мои эксперименты с быстродействием показали, что это не всегда так. В каком именно случае именно так, я пока не выяснил, но советую избегать в тех участках кода, где необходимо максимальное быстродействие, работы с данными, которые не могут быть выровнены в памяти.

Практически во всех прочих случаях преобразование типов происходит без потери производительности. В особенности это относится к преобразованияю типов указателей.

Например:

Код:
int main(void)
{
  MyClass* newptr = new MyClass();
  GetObj(newptr);
{

void GetObj(void* ptr)
{
  MyClass* var=(MyClass*)ptr;
  //some code
}
Подобная передача указателей вполне безопасна, если не обращать внимания на то, что void* сам по себе не типизирован и вы не имеете страховки, предоставляемой синтаксическим анализатором компилятора, которая может найти ошибку. Однако код будет работать без изменения производительности. Ведь мы всего лишь указали компилятору, что указатель ptr на самом деле является указателем на объект класса MyClass и теперь компилятор знает, как расположены данные по адресу указателя и какие функции могут работать с этими данными. И более ничего с этими данными не происходило.
__________________
Служу Советскому Союзу!

Хорошо смеется тот, кто стреляет первым! (танкистская мудрость)

Последний раз редактировалось [CCCP] Monster; 11.08.2010 в 04:07.
[CCCP] Monster вне форума  
Отправить сообщение для [CCCP] Monster с помощью Skype™ Ответить с цитированием