Hier, j'ai trouvé un article de Christoph Nahr intitulé «.NET Struct Performance» qui comparait plusieurs langages (C ++, C #, Java, JavaScript) pour une méthode qui ajoute deux structures de points ( double
tuples).
Il s'est avéré que la version C ++ prend environ 1000 ms pour s'exécuter (itérations 1e9), tandis que C # ne peut pas passer sous ~ 3000 ms sur la même machine (et fonctionne encore moins bien en x64).
Pour le tester moi-même, j'ai pris le code C # (et simplifié légèrement pour n'appeler que la méthode où les paramètres sont passés par valeur), et l'ai exécuté sur une machine i7-3610QM (augmentation de 3,1 GHz pour un seul cœur), 8 Go de RAM, Win8. 1, en utilisant .NET 4.5.2, RELEASE build 32 bits (x86 WoW64 puisque mon système d'exploitation est 64 bits). Voici 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 comme 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; } }
}
L'exécuter 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 devrait être intégrée, je me suis demandé comment le code fonctionnerait si je supprimais complètement les structures et que je mettais simplement le tout ensemble:
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 obtenu pratiquement le même résultat (en fait 1% plus lent après plusieurs tentatives), ce qui signifie que JIT-ter semble faire du bon travail en optimisant tous les appels de fonction:
Result: x=1000000001 y=1000000001, Time elapsed: 3200 ms
Cela signifie également que le benchmark ne semble mesurer aucune struct
performance et ne semble en fait mesurer que l' double
arithmétique de base (une fois que tout le reste a été optimisé).
Les trucs bizarres
Maintenant vient la partie étrange. Si j'ajoute simplement un autre chronomètre en dehors de la boucle (oui, je l'ai réduit à cette étape folle après plusieurs tentatives), le code s'exécute trois fois plus vite :
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
je me donnais de mauvais résultats car je peux clairement voir que cela se termine après une seule seconde.
Quelqu'un peut-il me dire ce qui pourrait se passer ici?
(Mettre à jour)
Voici deux méthodes dans le même programme, ce qui montre que la raison n'est pas 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();
}
}
Production:
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
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 vous en assurer).
(Mise à jour 4)
Suite aux commentaires de @ usr sur la réponse de @Hans, j'ai vérifié le démontage optimisé pour les deux méthodes, et elles sont assez différentes:
Cela semble montrer que la différence pourrait être due au compilateur agissant de manière amusante dans le premier cas, plutôt qu'à un double alignement de champ?
De plus, si j'ajoute deux variables (décalage total de 8 octets), j'obtiens toujours la même augmentation de vitesse - et il ne semble plus que cela soit lié à la mention d'alignement de champ par Hans Passant:
// 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);
}
double
variables locales , pas de struct
s, donc j'ai exclu les inefficacités des appels de structure / méthode.