Во-первых, в C# есть указатели, только тс-с-с! Об этом лучше не распространяться, особенно среди молодёжи, потому что это будет unsafe код со всеми вытекающими последствиями.
Во-вторых, со времён C известно, что арифметика указателей положительно сказывается на быстродействии. И поэтому в С выражение a[i] есть эквивалент *(a+i). Про C++ и тем более C# такого сказать уже нельзя: оператор [] может быть перегружен и нагружен всяческими дополнительными вещами, как-то проверки диапазонов. Кроме того, в C# объекты под управлением сборщика мусора могут перемещаться по памяти с целью дефрагментации, и решивший вдруг поработать сборщик мусора не оставит шансов добиться быстродействия.
Однако и в C# есть неафишируемые подходы, позволяющие выполнять код без проверок и с простой арифметикой. Но всё равно C/C++ за счёт эффективности компилятора предпочтительнее для интенсивных вычислений, нежели C#.
Итак, общая инфраструктура:
using System;
using System.Runtime.InteropServices;
namespace Test
{
class Program
{
delegate void Test();
const int mx = 10000, my = 10000, md = 1;
static short[] a = new short[mx * my];
static double[] b = new double[mx * my];
unsafe static void Perform(Test test)
{
double bestTime = double.MaxValue;
for (int i = 0; i < 3; ++i)
{
DateTime begin = DateTime.Now;
test();
DateTime end = DateTime.Now;
double time = (end - begin).TotalSeconds;
if(time < bestTime)
{
bestTime = time;
}
}
Console.WriteLine("{0:##.###} s ", bestTime);
}
unsafe static void Main(string[] args)
{
for (int y = 0; y < my; ++y)
for (int x = 0; x < mx; ++x)
{
a[y * mx + x] = 1;
}
Perform(Test1);
Perform(Test2);
Perform(Test3);
Perform(Test4);
Console.ReadKey();
}
}
}
#include <climits>
#include <iostream>
#include <Windows.h>
using namespace std;
typedef void (*Test)();
const int mx = 10000, my = 10000, md = 1;
short *a;
double *b;
void Perform(Test test)
{
double bestTime = DBL_MAX;
for (int i = 0; i < 3; ++i)
{
DWORD begin = GetTickCount();
test();
DWORD end = GetTickCount();
double time = (end - begin) / 1000.0;
if(time < bestTime)
{
bestTime = time;
}
}
std::cout << bestTime << " s " << std::endl;;
}
int main()
{
a = new short[mx * my];
b = new double[mx * my];
for (int y = 0; y < my; ++y)
for (int x = 0; x < mx; ++x)
{
a[y * mx + x] = 1;
}
Perform(Test1);
Perform(Test2);
//Perform(Test3);
Perform(Test4);
delete[] a;
delete[] b;
cin.get();
return 0;
}
Теперь сравниваем алгоритм, задача которого в массиве b записать усреднения 3х3 областей из a в обратном порядке. Замеры времени работы производились на одной и той же машине, платформа x86. C++ и C# из VS 2012, .NET 4.5, оптимизации включены в обоих случаях, сборка Release.
Первый тест: решение в лоб
static void Test1()
{
for (int y = md; y < my - md; ++y)
for (int x = md; x < mx - md; ++x)
{
b[(my - y - 1) * mx + (mx - x - 1)] = 0.0;
for (int dy = -md; dy <= md; ++dy)
for (int dx = -md; dx <= md; ++dx)
{
b[(my - y - 1) * mx + (mx - x - 1)] += a[(y + dy) * mx + (x + dx)];
}
b[(my - y - 1) * mx + (mx - x - 1)] /= (md * 2 + 1) * (md * 2 + 1);
}
}
Работает 9.7 секунды.
void Test1()
{
for (int y = md; y < my - md; ++y)
for (int x = md; x < mx - md; ++x)
{
b[(my - y - 1) * mx + (mx - x - 1)] = 0.0;
for (int dy = -md; dy <= md; ++dy)
for (int dx = -md; dx <= md; ++dx)
{
b[(my - y - 1) * mx + (mx - x - 1)] += a[(y + dy) * mx + (x + dx)];
}
b[(my - y - 1) * mx + (mx - x - 1)] /= (md * 2 + 1) * (md * 2 + 1);
}
}
Работает 1.3 секунды.
Второй тест: попытка сократить вычисления, вручную вытаскивая из циклов повторяющиеся расчёты и устраняя выделение/удаление переменных на стеке.
static void Test2()
{
int x, y, dx, dy, oa, ob, pb, oda, myl, mxl, s;
myl = my - md;
mxl = mx - md;
s = md * 2 + 1;
s *= s;
for (y = md; y < myl; ++y)
{
oa = y * mx;
ob = (my - y - 1) * mx;
for (x = md; x < mxl; ++x)
{
pb = ob + (mx - x - 1);
b[pb] = 0.0;
for (dy = -md; dy <= md; ++dy)
{
oda = oa + dy * mx;
for (dx = -md; dx <= md; ++dx)
{
b[pb] += a[oda + (x + dx)];
}
}
b[pb] /= s;
}
}
}
Работает 8.9 секунды.
void Test2()
{
int x, y, dx, dy, oa, ob, pb, oda, myl, mxl, s;
myl = my - md;
mxl = mx - md;
s = md * 2 + 1;
s *= s;
for (y = md; y < myl; ++y)
{
oa = y * mx;
ob = (my - y - 1) * mx;
for (x = md; x < mxl; ++x)
{
pb = ob + (mx - x - 1);
b[pb] = 0.0;
for (dy = -md; dy <= md; ++dy)
{
oda = oa + dy * mx;
for (dx = -md; dx <= md; ++dx)
{
b[pb] += a[oda + (x + dx)];
}
}
b[pb] /= s;
}
}
}
Работает 1.3 секунды.
Компилятор C++ все эти усовершенствования игнорирует - и правильно делает. Комплиятор C# тоже практически игнорирует, хотя 5% выиграли.
В C# можно особой директивой отключить разные проверки переполнения и т.п. В C++ аналога нет, поэтому третий тест лишь в одном варианте:
static void Test3()
{
unchecked
{
int x, y, dx, dy, oa, ob, pb, oda, myl, mxl, s;
myl = my - md;
mxl = mx - md;
s = md * 2 + 1;
s *= s;
for (y = md; y < myl; ++y)
{
oa = y * mx;
ob = (my - y - 1) * mx;
for (x = md; x < mxl; ++x)
{
pb = ob + (mx - x - 1);
b[pb] = 0.0;
for (dy = -md; dy <= md; ++dy)
{
oda = oa + dy * mx;
for (dx = -md; dx <= md; ++dx)
{
b[pb] += a[oda + (x + dx)];
}
}
b[pb] /= s;
}
}
}
}
Работает 9.2 секунды, т.е. разницы с предыдущим тестом фактически нет, а может даже и стало хуже.
Наконец, последний тест без оператора [] с арифметикой указателей.
unsafe static void Test4()
{
unchecked
{
GCHandle ha = GCHandle.Alloc(a, GCHandleType.Pinned);
GCHandle hb = GCHandle.Alloc(b, GCHandleType.Pinned);
short* pa = (short*)ha.AddrOfPinnedObject();
double* pb = (double*)hb.AddrOfPinnedObject();
int x1 = md, xn = (mx - 1) - md;
int y1 = md * mx, yn = (my - 1 - md) * mx;
int dx1 = -md, dxn = md;
int dy1 = -md * mx, dyn = md * mx;
short* pya, pan, pxya, pyan, pdy, pdn, pdxy, pdyn;
double* pyb, pxyb;
int s;
s = md * 2 + 1;
s *= s;
for (pya = pa + y1, pan = pa + yn, pyb = pb + yn; pya <= pan; pya += mx, pyb -= mx)
for (pxya = pya + x1, pyan = pya + xn, pxyb = pyb + xn; pxya <= pyan; ++pxya, --pxyb)
{
*pxyb = 0.0;
for (pdy = pxya + dy1, pdn = pxya + dyn; pdy <= pdn; pdy += mx)
for (pdxy = pdy + dx1, pdyn = pdy + dxn; pdxy <= pdyn; ++pdxy)
{
*pxyb += *pdxy;
}
*pxyb /= s;
}
ha.Free();
hb.Free();
}
}
Работает 6.9 секунды.
void Test4()
{
short *pa = a;
double *pb = b;
int x1 = md, xn = (mx - 1) - md;
int y1 = md * mx, yn = (my - 1 - md) * mx;
int dx1 = -md, dxn = md;
int dy1 = -md * mx, dyn = md * mx;
short *pya, *pan, *pxya, *pyan, *pdy, *pdn, *pdxy, *pdyn;
double *pyb, *pxyb;
int s;
s = md * 2 + 1;
s *= s;
for (pya = pa + y1, pan = pa + yn, pyb = pb + yn; pya <= pan; pya += mx, pyb -= mx)
for (pxya = pya + x1, pyan = pya + xn, pxyb = pyb + xn; pxya <= pyan; ++pxya, --pxyb)
{
*pxyb = 0.0;
for (pdy = pxya + dy1, pdn = pxya + dyn; pdy <= pdn; pdy += mx)
for (pdxy = pdy + dx1, pdyn = pdy + dxn; pdxy <= pdyn; ++pdxy)
{
*pxyb += *pdxy;
}
*pxyb /= s;
}
}
Работает 4.5 секунды.
Из чего следует, что автоматическую оптимизацию компилятора C++ мы своей ручной безнадёжно сломали, а вот для C# получили заметное ускорение. Причём несложным экспериментом можно убедиться, что оно зависит исключительно от неиспользования оператора [], а не от защиты массивов от активности сборщика мусора.
Любопытно также, что Debug сборка C++ и Release сборка C# работают одинаково по времени. Ну это лишь совпадение.
Также стоит отметить, что при объявлении указателей звёздочка * в C# - элемент имени типа, а в C++ - элемент декларации переменной. На это можно напороться, если объявлять много переменных в одной строке через запятую - см. выше.
Мораль:
- Если нужны интенсивные вычисления с беганием по большим и не влезающим в кэш массивам (например, при обработке видеопотоков), лучше их писать на C++.
- Если в подобных задачах от C# отказаться никак, то ручная оптимизация за счёт выноса повторно используемых значений во внешние циклы и использование указателей, как показано выше, способно дать некоторое ускорение примерно на 20-25%.