Друг
Первые программисты манипулировали такими понятиями как «0» и «1» и это было долго и непонятно. Человеческая лень предложила при помощи ноликов и единичек написать программу преобразующую MOV, ADD, JMP в новые нолики и единички, понятные процессору.
В дальнейшем лень не дремала и призывала вместо 6-10 строк ассемблера писать 1 команду высокого уровня. Затем появились еще более ленивые вещи. ООП, базы данных и так далее, и так далее. Программы становились все понятнее и понятнее, поиск ошибок сокращался в разы, благодаря различным фреймверкам увеличивалась скорость разработки. Высокоуровневая абстракция шествовала по планете. И все были счастливы?
Враг
Но враг не дремал! В один прекрасный момент программист сталкивается с проблемой, которую на данном уровне абстракции нельзя ни то что решить, но даже понять откуда у этой проблемы растут ноги. Для того, чтобы понять что происходит приходится лезть в дебри ноликов и единичек. Примеры? Да пожалуйста (все примеры даны на языке C#).
Пример 1
static void Main(string[] args)
{
double tenth = 0.1;
double first = 0;
for (int i = 0; i < 10; i++)
{
first += tenth;
}
double eighth = 0.125;
double second = 0;
for (int i = 0; i < 8; i++)
{
second += eighth;
}
Console.WriteLine(first == second);
Console.WriteLine("0.1 * 10 = {0}\n0.125 * 8 = {1}", first, second);
Console.ReadKey();
}Попробуйте предположить, что мы увидим на экране.
Попробовали? И что у вас получилось? Так вот, для тех кто предположил, ну и для тех кто поленился и просто дочитал до этого места, я информирую, мы увидим — False.
Обратили внимание, что значения совпадают? В принципе если заменить 10 на 100, а 8 на 80, то мы увидим, что значения отличаются, но здесь то О_о
Вот он уровень нулей и единиц. В принципе, понимая как представляются числа с плавающей точкой, понятно, что 1/8 это ни что иное как 2 в -3 степени и складываться она будет без погрешностей, а вот 0.1 представить в виде суммы целых степеней двойки в разрядности современного double не получается. Вот здесь и набегает погрешность.
Продолжим?
Пример 2
static void Main(string[] args)
{
int n = 10000;
int[,] array1 = new int[n, n];
int[,] array2 = new int[n, n];
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
array1[i, j] = 0;
array2[i, j] = 0;
}
}
DateTime begin = DateTime.Now;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
array1[j, i]++;
}
}
Console.WriteLine("Время работы [j, i] = {0}", DateTime.Now.Subtract(begin).TotalSeconds);
begin = DateTime.Now;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
array1[i, j]++;
}
}
Console.WriteLine("Время работы [i, j] = {0}", DateTime.Now.Subtract(begin).TotalSeconds);
Console.ReadKey();
}Самое удивительное, что изменив порядок индексов при доступе к элементам массива мы получаем изменение скорости работы почти в два раза (на моем компьютере первый цикл отработал за 2,16 с; второй за 1,13 с). При увеличении n до 11000, разница составляет уже 5 раз.
От куда такая разница? Опять же с уровня нулей и единиц. В данном случае проявляется работа с кэшем и то, что двухмерный массив в памяти на самом деле одномерен.
Пример 3
С появлением generic типов этот пример может и не актуален, но все таки не удержусь.
class MyPack
{
public int Value { get; set; }
}
static void Main(string[] args)
{
int n = 100000;
object[] array1 = new object[n];
object[] array2 = new object[n];
for (int i = 0; i < n; i++)
{
array1[i] = 0;
array2[i] = new MyPack() { Value = 0 };
}
DateTime begin = DateTime.Now;
for (int i = 0; i < n; i++)
{
array1[i] = (int)array1[i] + 1;
}
Console.WriteLine("Время работы c int = {0}", DateTime.Now.Subtract(begin).TotalSeconds);
begin = DateTime.Now;
for (int i = 0; i < n; i++)
{
((MyPack)array2[i]).Value = ((MyPack)array2[i]).Value + 1;
}
Console.WriteLine("Время работы с MyPack = {0}", DateTime.Now.Subtract(begin).TotalSeconds);
Console.ReadKey();
}В первом случае время выполнения составило 0,017 секунды, во втором 0,005 секунды. Здесь вступает в силу не на столько низкоуровневые вещи, а особенности работы с преобразованием простых типов к типу object (кто заинтересуется можно почитать про упаковку и распаковку).
Выводы
Начну с того, что я не считаю высокоуровневую абстракцию злом. И название статьи, если вы обратили внимание, именно о том, что высокоуровневая абстракция нам друг, и чтобы она не не превратилась во врага, пойду ка я почитаю про новые архитектуры процессоров Intel.
P.s. Во время написания статьи выходил в коридор, где столкнулся с коллегой. Так вот, он подошел с проблемой, что при разработке приложения для Windows Mobile приложение адекватно работает до тех пор, пока КПК не уходит в sleep. При выводе его в нормальный режим приложения в памяти уже нет, сообщений об ошибках нет, ничего нет. Вы как думаете по чему? Я думаю, что из-за дыры в высокоуровневой абстракции.