@youkerni
Unity3D developer

Откуда берется overhead?

Вчера ради эксперимента решил провести ряд тестов.
У меня есть код на шарпах:
СS code
class Program
    {
        static int c;
        static void Swap(ref int i, ref int j)
        {
            c = i;
            i = j;
            j = c;
        }

        static void Sort(int[] arr)
        {
            int temp = 0;
            for (int i = 0; i < arr.Length - 1; i++)
            {
                for (int j = 0; j < arr.Length - i - 1; j++)
                {
                    if (arr[j] > arr[j + 1])
                    {
                        // меняем элементы местами
                        Swap(ref arr[j], ref arr[j + 1]);
                        //temp = arr[j];
                        //arr[j] = arr[j + 1];
                        //arr[j + 1] = temp;
                    }
                }
            }
        }

        public static void Main(string[] args)
        {
            int[] arr = new int[10000];
            var rand = new Random();
            for (int i = 0; i < arr.Length; i++)
                arr[i] = rand.Next();

            var sw = new Stopwatch();
            sw.Start();
            Sort(arr);
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
            Console.ReadKey();
        }
    }



И аналогичный код на плюсах
CPP code
#include <iostream>
#include <time.h>
#include <chrono>
#include <ctime>

using namespace std;

int temp;
void Swap(int& a, int &b)
{
        с = a;
        a = b;
        b = c;
}

int main()
{
	srand(time(0));
	int *arr; // указатель для выделения памяти под массив
	int size = 10000; // размер массива

	arr = new int[size]; 

	for (int i = 0; i < size; i++) 
		arr[i] = rand();

	auto start = chrono::system_clock::now();
	// Сортировка массива пузырьком
	for (int i = 0; i < size - 1; i++) 
	{
		for (int j = 0; j < size - i - 1; j++) 
		{
			if (arr[j] > arr[j + 1]) 
			{
                                //swap(arr[j], arr[j+1]);
				Swap(arr[j], arr[j + 1]);

				/*temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;*/
			}
		}
	}
	auto end = chrono::system_clock::now();
	chrono::duration<double> elapsed_seconds = end - start; 
	time_t end_time = chrono::system_clock::to_time_t(end);

	cout << "finished computation at " << elapsed_seconds.count() << "s\n";
	system("pause");
}


Проведя замеры разных ситуаций получил следующие результаты:
1) С# с вызовом swap (c объявлена в swap) - ~800мс
2) C# без метода swap (замена в теле for) - ~495мс
3) С# с вызовом swap (c - статическая переменная (без аллокаций на стеке)) - ~590мс
4) С# с вызовом swap (без использования доп. переменной) - ~622мс

5) С++ с вызовом swap (стандартный из std) - ~4000мс
6) С++ с вызовом swap (самописный, аргументы передаются по ссылке, c объявлена в swap) - ~1730мс
7) C++ без метода swap (замена в теле for) - ~470мс
8) С++ с вызовом swap (c - статическая переменная (без аллокаций на стеке)) - ~1690мс
9) С++ с вызовом swap (без использования доп. переменной) - ~1780мс

И самый интересный случай произошел когда я случайно инициализировал переменную объявленную в swap нулем, вместо значения одной из переменных:
10) C++ без инициализации временной переменной 230мс
11) C# без инициализации временной переменной 250мс

Код второго случая в общем виде:
//в c# соответственно ref
void Swap(int& a, int &b)
{
        //тут забыл инициализировать
	int temp = 0;
	a = b;
	b = temp;
}


Собственно, вопросы:
1) Почему вызов метода(функции) дает такой большой оверхед по сравнению с вычислениями в теле for?
2) Правильно ли я понимаю, что 10 и 11 случай компилятор хорошо оптимизирует до чего-то подобного
spoiler
void Swap(int& a, int &b)
{
	a = b;
	b = 0;
}

из-за чего мы избегаем аллокаций на стеке и лишнего побитового копирования?
3) Что нужно было натворить в std::swap что бы она выполнялась в разы дольше маленького самописного Swap?
  • Вопрос задан
  • 245 просмотров
Решения вопроса 2
@MarkusD Куратор тега C++
все время мелю чепуху :)
Стандарт C++ предписывает статическое расположение для глобальных и статических переменных. Это означает целую массу побочных эффектов для генерации использующего переменную кода.

Для примера можно взять твою функцию swap и посмотреть на то, как компилятор будет генерировать код даже с учетом оптимизации.
Смотрим первый пример
void swap( int& left, int& right )
{
    static int t = left;
    left = right;
    right = t;
}

И вот результат обработки clang.
swap(int&, int&):                            # @swap(int&, int&)
        pushq   %r14
        pushq   %rbx
        pushq   %rax
        movq    %rsi, %rbx
        movq    %rdi, %r14
        movb    guard variable for swap(int&, int&)::t(%rip), %al
        testb   %al, %al
        je      .LBB0_1
.LBB0_3:
        movl    (%rbx), %eax
        movl    %eax, (%r14)
        movl    swap(int&, int&)::t(%rip), %eax
        movl    %eax, (%rbx)
        addq    $8, %rsp
        popq    %rbx
        popq    %r14
        retq
.LBB0_1:
        movl    $guard variable for swap(int&, int&)::t, %edi
        callq   __cxa_guard_acquire
        testl   %eax, %eax
        je      .LBB0_3
        movl    (%r14), %eax
        movl    %eax, swap(int&, int&)::t(%rip)
        movl    $guard variable for swap(int&, int&)::t, %edi
        callq   __cxa_guard_release
        jmp     .LBB0_3
Смотрим второй пример
void swap( int& left, int& right )
{
    int t = left;
    left = right;
    right = t;
}

Вот результат выдачи от clang.
swap(int&, int&):                            # @swap(int&, int&)
        movl    (%rdi), %eax
        movl    (%rsi), %ecx
        movl    %ecx, (%rdi)
        movl    %eax, (%rsi)
        retq


Не обязательно быть специалистом в ассемблере чтобы увидеть прямую разницу в сгенерированном коде.
На самом деле, если t объявить именно как глобальную переменную, то сгенерированный код станет немного легче, но он все равно будет объемнее и тяжелее кода с локальной переменной. Уже просто потому что глобальная переменная находится в глобальной памяти, а запросы из процессора в ОЗУ - сравнительно долгая операция.
Сравнение скорости доступа к памяти
cacheanim.gif
Ответ написан
vt4a2h
@vt4a2h Куратор тега C++
Senior software engineer (C++/Qt/boost)
Во втором случае, я не думаю, что это правильно сортирует, т.к. в переменную b всегда ноль записывается. Переменную tmp нужно переменной a инициализировать.

Время нужна считать с учётом включенных оптимизаций. Например в релизном режиме, с флагом -Ofast или -O3 результаты будут сильно быстрее (в том числе и для std::swap). С похожими параметрами обычно компилируются программы, которые поставляются конечному пользователю.

В режиме отладки, который обычно активирован по умолчанию, компилятор не применяет множество оптимизаций и генерирует отладочную информацию, чтобы программисту легче было разрабатывать. Узнать, какой код был сгенерирован в зависимости от флагов компиляторы, можно тут: https://godbolt.org/ .

Чтобы понять, какая часть программы потребляет больше всего ресурсов, пользуйтесь профайлерами. Например valgrind с соответствующими флагами.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы