Hier, j'ai trouvé un Article de Christoph Nahr intitulé "Performances des structures .NET". qui a évalué plusieurs langages (C++, C#, Java, JavaScript) pour une méthode qui ajoute deux structs ponctuels ( double
tuples).
Il s'est avéré que la version C++ met environ 1000 ms à s'exécuter (1e9 itérations), alors que C# ne peut descendre en dessous de ~3000 ms sur la même machine (et se comporte encore plus mal en x64).
Pour le tester moi-même, j'ai pris le code C# (et l'ai légèrement simplifié pour n'appeler que la méthode où les paramètres sont passés par valeur), et je l'ai exécuté sur une machine i7-3610QM (3.1Ghz boost pour single core), 8GB RAM, Win8.1, utilisant .NET 4.5.2, RELEASE build 32-bit (x86 WoW64 puisque mon OS est 64-bit). Il s'agit de la version simplifiée :
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Point AddByVal(Point a, Point b)
{
return new Point(a.X + b.Y, a.Y + b.X);
}
public static void Main()
{
Point a = new Point(1, 1), b = new Point(1, 1);
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
}
Avec Point
défini aussi simplement :
public struct Point
{
private readonly double _x, _y;
public Point(double x, double y) { _x = x; _y = y; }
public double X { get { return _x; } }
public double Y { get { return _y; } }
}
Son exécution produit des résultats similaires à ceux de l'article :
Result: x=1000000001 y=1000000001, Time elapsed: 3159 ms
Première observation étrange
Puisque la méthode doit être inlined, je me suis demandé comment le code se comporterait si je supprimais complètement les structs et si j'inlined simplement le tout :
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
public static void Main()
{
// not using structs at all here
double ax = 1, ay = 1, bx = 1, by = 1;
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
{
ax = ax + by;
ay = ay + bx;
}
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
ax, ay, sw.ElapsedMilliseconds);
}
}
Et j'ai obtenu pratiquement le même résultat (en fait 1% plus lent après plusieurs essais), ce qui signifie que le JIT-ter semble faire un bon travail d'optimisation de tous les appels de fonction :
Result: x=1000000001 y=1000000001, Time elapsed: 3200 ms
Cela signifie également que l'indice de référence ne semble mesurer aucune struct
et ne semblent en fait mesurer que les performances de base double
arithmétique (après que tout le reste ait été optimisé).
Les trucs bizarres
Maintenant vient la partie bizarre. Si j'ajoute simplement un autre chronomètre en dehors de la boucle (oui, j'ai réduit le problème à cette étape folle après plusieurs tentatives), le code s'exécute trois fois plus rapide :
public static void Main()
{
var outerSw = Stopwatch.StartNew(); // <-- added
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
outerSw.Stop(); // <-- added
}
Result: x=1000000001 y=1000000001, Time elapsed: 961 ms
C'est ridicule ! Et ce n'est pas comme si Stopwatch
me donne des résultats erronés car je peux clairement voir qu'elle se termine après une seule seconde.
Quelqu'un peut-il me dire ce qui peut se passer ici ?
(Mise à jour)
J'ai également supposé que cela avait quelque chose à voir avec le JITting, mais répéter le test plusieurs fois (sans le chronomètre extérieur) est lent dans tous les cas :
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Point AddByVal(Point a, Point b)
{
return new Point(a.X + b.Y, a.Y + b.X);
}
public static void Main()
{
Test();
Test();
Test();
}
private static void Test()
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
}
Sortie :
Result: x=1000000001 y=1000000001, Time elapsed: 3251 ms
Result: x=1000000001 y=1000000001, Time elapsed: 3249 ms
Result: x=1000000001 y=1000000001, Time elapsed: 3250 ms
Encore une fois, avec le chronomètre extérieur ajouté, nous obtenons le coup de pouce magique :
public static void Main()
{
Test();
Test();
Test();
}
private static void Test()
{
var outerSw = Stopwatch.StartNew();
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms", a.X, a.Y, sw.ElapsedMilliseconds);
outerSw.Stop();
}
Result: x=1000000001 y=1000000001, Time elapsed: 979 ms
Result: x=1000000001 y=1000000001, Time elapsed: 982 ms
Result: x=1000000001 y=1000000001, Time elapsed: 978 ms
(Mise à jour 2)
Voici deux méthodes dans le même programme, ce qui montre que la raison n'est pas le JITting :
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Point AddByVal(Point a, Point b)
{
return new Point(a.X + b.Y, a.Y + b.X);
}
public static void Main()
{
Test1();
Test2();
Console.WriteLine();
Test1();
Test2();
}
private static void Test1()
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test1: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
private static void Test2()
{
var swOuter = Stopwatch.StartNew();
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test2: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
swOuter.Stop();
}
}
Sortie :
Test1: x=1000000001 y=1000000001, Time elapsed: 3242 ms
Test2: x=1000000001 y=1000000001, Time elapsed: 974 ms
Test1: x=1000000001 y=1000000001, Time elapsed: 3251 ms
Test2: x=1000000001 y=1000000001, Time elapsed: 972 ms
(Mise à jour 3)
Voici un pastebin. Vous devez l'exécuter en tant que version 32 bits sur .NET 4.x (il y a quelques vérifications dans le code pour s'en assurer).
(Solution, peut-être ?) (lire La réponse de @HansPassant pour plus de détails)
Selon @Hans, le ralentissement est dû au fait que les variables 64 bits ne sont pas toujours alignées dans les applications 32 bits. Si j'ajoute une variable 32 bits à l'une des méthodes de test, elle sera aussi rapide que la version C++ :
private static void Test3()
{
var magical_speed_booster = "whatever";
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test2: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
GC.KeepAlive(magical_speed_booster);
}
(Mise à jour 4)
Suite aux commentaires de @usr sur la réponse de @Hans, j'ai vérifié le désassemblage optimisé pour les deux méthodes, et ils sont assez différents :
Cela semble montrer que la différence pourrait être due à un comportement bizarre du compilateur dans le premier cas, plutôt qu'à l'alignement du double champ ?
Aussi, si j'ajoute dos (décalage total de 8 octets), j'obtiens toujours le même gain de vitesse - et il ne semble plus que ce soit lié à l'alignement des champs :
// this is still fast?
private static void Test3()
{
var magical_speed_booster_1 = "whatever";
var magical_speed_booster_2 = "whatever";
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test2: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
GC.KeepAlive(magical_speed_booster_1);
GC.KeepAlive(magical_speed_booster_2);
}