Zeitkritische Momente mit dem Netduino

Quadrocopter_48
USB TTL UART, Netduino mit Shield und Schaltnetzteil für Tests.

Bei einigen Anwendungen kommt es vor, dass eine Iteration möglichst wenig Zeit benötigen soll. Das bedeutet, dass man sich mit Code Optimierung beschäftigen muss, in dem man zumeist die gewohnte Art zu Programmieren überdenken muss. Grundsätzlich sollte man das nicht tun, wenn man Anfänger ist, und dann auch nicht wenn man professioneller Entwickler ist. Es sei denn, die Anforderung erfordert diese Optimierung. In der Entwicklung für meinen Quadrocopter stieß ich auf dieses Problem, dass eine Iteration viel zu lange dauert. Zwar sauber geschrieben, jedoch nicht Performance-optimiert, kam ich auf ca. 40ms bis 45ms pro Iteration. Das ergibt pro Sekunde 22 bis 25 Durchläufe. Nach den Optimierungen kam ich auf Iterationslaufzeiten von 11ms bis 22ms. Diese weite Zeitspanne entstand durch dynamische Verarbeitung von Byte Werten beim Senden und Empfangen, was jedoch ein anderes Thema abbilden würde. Zunächst benötigen wir eine Methode, um herauszufinden, wie lange ein Vorgang an Zeit benötigt.

public class Stopwatch
{
    private static long _Ticks_Start = 0;
    private static long _TicksPerMillisecound = System.TimeSpan.TicksPerMillisecond;

    public static long GetElapsedMillisecounds()
    {
         long result = (Microsoft.SPOT.Hardware.Utility.GetMachineTime().Ticks
            - _Ticks_Start) / _TicksPerMillisecound;

         _Ticks_Start = Microsoft.SPOT.Hardware.Utility.GetMachineTime().Ticks;

        return result;
    }

    public static long GetElapsedMicrosecounds()
    {
         long result = (Microsoft.SPOT.Hardware.Utility.GetMachineTime().Ticks
            - _Ticks_Start) / (_TicksPerMillisecound / 1000);

         _Ticks_Start = Microsoft.SPOT.Hardware.Utility.GetMachineTime().Ticks;

         return result;
    }
}

Die Klasse habe ich aus dem Netduino Forum und ist für dieses Beispiel angepasst.

Kommen wir zu einigen Beispielen, in dem einfach einer Integer-variablen ein Wert zugewiesen wird der dann durch einen anderen dividiert wird. Nach 100 Durchläufen werden die Durchschnittszeiten der 6 Beispieel ausgegeben. Der Grund ist, dass das Ergebnis nicht konstant bleibt und durch weitere Einflüsse beeinträchtigt ist.

public class Program
{
    public static void Main()
    {
        long timeResult = 0;
        int result = 0;

        long countIteration = 1;
        long avarageExample1 = 0;
        long avarageExample2 = 0;
        long avarageExample3 = 0;
        long avarageExample4 = 0;
        long avarageExample5 = 0;
        long avarageExample6 = 0;

        while (true)
        {

           // Beispiel mit einer Devisions Rechnung in Millisekunden
            Stopwatch.GetElapsedMillisecounds();

            result = 12345;
            result = result / 67890;

            timeResult = Stopwatch.GetElapsedMillisecounds();
            Debug.Print("Beispiel 1: " + timeResult +
                " Millisekunde, Freier Speicher: " + Debug.GC(true) + "kb");
            avarageExample1 += timeResult;

           // Beispiel mit einer Devisions Rechnung in Mikrosekunden
            Stopwatch.GetElapsedMillisecounds();

            result = 12345;
            result = result / 67890;

            timeResult = Stopwatch.GetElapsedMicrosecounds();
            Debug.Print("Beispiel 2: " + timeResult +
                " Mikrosekunden, Freier Speicher: " + Debug.GC(true) + "kb");
            avarageExample2 += timeResult;

            // Beispiel mit einer Devisions Rechnung
            // über eine Methode und den Integer kopieren.

            Stopwatch.GetElapsedMillisecounds();

            result = 12345;
            result = GetDivided(result);

            timeResult = Stopwatch.GetElapsedMicrosecounds();
            Debug.Print("Beispiel 3: " + timeResult +
                " Mikrosekunden, Freier Speicher: " + Debug.GC(true) + "kb");
            avarageExample3 += timeResult;

            // Beispiel mit einer Devisions Rechnung
            // über eine Methode und den Integer referenzieren.
            Stopwatch.GetElapsedMillisecounds();

            result = 12345;
            GetDividedByRef(ref result);

            timeResult = Stopwatch.GetElapsedMicrosecounds();
            Debug.Print("Beispiel 4: " + timeResult +
                " Mikrosekunden, Freier Speicher: " + Debug.GC(true) + "kb");
            avarageExample4 += timeResult;

            // Beispiel mit einer Devisions Rechnung
            // über eine Methode aus einer anderen Klasse und den Integer kopieren.
            Stopwatch.GetElapsedMillisecounds();

            result = 12345;
            result = MethodeInOtherClass.GetDivided(result);

            timeResult = Stopwatch.GetElapsedMicrosecounds();
            Debug.Print("Beispiel 5: " + timeResult +
                " Mikrosekunden, Freier Speicher: " + Debug.GC(true) + "kb");
            avarageExample5 += timeResult;

            // Beispiel mit einer Devisions Rechnung
            // über eine Methode aus einer anderen Klasse und den Integer referenzieren.

            Stopwatch.GetElapsedMillisecounds();

            result = 12345;
            MethodeInOtherClass.GetDividedByRef(ref result);

            timeResult = Stopwatch.GetElapsedMicrosecounds();
            Debug.Print("Beispiel 6: " + timeResult +
                " Mikrosekunden, Freier Speicher: " + Debug.GC(true) + "kb");
            avarageExample6 += timeResult;

            if (countIteration > 100)
            {
                // Durchschnitts Ergebnisse
                Debug.Print("Beispiel 1, Durchscnittliche Zeit: " +
                    (avarageExample1 / countIteration) + " Millisekunde");
                Debug.Print("Beispiel 2, Durchscnittliche Zeit: " +
                    (avarageExample2 / countIteration) + " Mikrosekunden");
                Debug.Print("Beispiel 3, Durchscnittliche Zeit: " +
                    (avarageExample3 / countIteration) + " Mikrosekunden");
                Debug.Print("Beispiel 4, Durchscnittliche Zeit: " +
                    (avarageExample4 / countIteration) + " Mikrosekunden");
                Debug.Print("Beispiel 5, Durchscnittliche Zeit: " +
                    (avarageExample5 / countIteration) + " Mikrosekunden");
                Debug.Print("Beispiel 6, Durchscnittliche Zeit: " +
                    (avarageExample6 / countIteration) + " Mikrosekunden");
            }

            countIteration++;
        }
    }

   private static int GetDivided(int result)
    {
        return result / 67890;
    }
    private static void GetDividedByRef(ref int result)
    {
        result = result / 67890;
    }
}

public static class MethodeInOtherClass
{
    public static int GetDivided(int result)
    {
        return result / 67890;
    }
    public static void GetDividedByRef(ref int result)
    {
        result = result / 67890;
    }
}

image
Ermittelt eine Durchschnittszeit, da das Ergebnis schwankt.

Das erste Beispiel zeigt das Ergebnis in Millisekunden, jedoch reicht das für mein zeitkritisches Thema nicht aus. Daher zeigt das zweite Beispiel das Ergebnis in Mikrosekunden an. Sieht man sich die Durchschnittszeit an, dann passt diese Vorgangsrechnung praktisch 3 mal pro Millisekunde. Die Beispiele 3 bis 6 zeigen eher unwesentliche Unterschiede. Hier zeigt der Vorgang über eine Methode eine längere Zeitspanne von ca. 60 Mikrosekunden, egal wo sich die Methode befindet. Geht man die mehrmals errechneten Durchschnittszeiten durch, ist zu erkennen, dass die Ergebnisse schwanken. Vielleicht irre ich mich da, aber ich konnte erkennen, dass es nur beim Debuggen auftrat. Stabil blieben die Werte, nachdem ich die Ergebnisse immer über eine UART Verbindung an den PC gesendet habe und noch ein wenig besser wurden die Ergebnisse, wenn der Netduino über eine andere Spannungsversorgung von mindestens 7,5V angeschlossen wurde. Ich selbst verwendete ein 12V / 1A Schalt Netzteil oder einen 9V / mAh Block Akku für meine Anwendungen und Tests. Für die folgende Ausgabe habe ich noch 3 weitere Arten des Methodenaufrufes eingesetzt.

  • Beispiel 7 ist das Ergebnis über einen Methodenaufruf in einer anderen DLL.
  • Beispiel 8 ist das Ergebnis über einen Methodenaufruf mit zwei übergebenen Werten.
  • Beispiel 9 ist das Ergebnis über einen Methodenaufruf, wobei der referenzierte Integer vorher angelegt wird.
image
Ergebnis ohne Debug und Netduino weiterhin an USB angeschlossen.

Seltsamerweise war das Ergebnis unwesentlich besser.

image
Ergebnis mit einem 12V Schaltnetzteil, mit dem Akku sah die Ausgabe geradezu identisch aus.

Bei diesen Beispielen mit den verschiedenen Spannungsquellen, lohnt sich der Test allerdings erst, wenn der Netduino aufwendigere Aufgaben zu erledigen hat, auf dem weitere Module gesteckt sind und viele Algorithmen verwendet werden.

image
Akkus und Schaltnetzteile stellen zum Glück vernachlässigbare Unterschiede.

Zum Abschluss diesen Blog Eintrages bildet sich eines auf jeden Fall klar ab: Es kostet Zeit! Und je aufwendiger das Programm, um so exponentiell steigt die Möglichkeit der Optimierung und macht erst Sinn, wenn das Ziel diese Anforderung unausweichlich benötigt.

Beispiel Projekt zum Downloaden. (.NET Micro Framework 4.2)

Kommentare

Beliebte Posts aus diesem Blog

Arduino Control (Teil 5) - PWM Signal einlesen

Angular auf dem Raspberry Pi

RC Fahrtenregler für Lego Kettenfahrzeug