Wie schnell man Speicher falsch kopieren kann

Neulich habe ich hier auf der Homepage einen Eintrag über das Lesen von Frames aus einem Video per Media Foundation geschrieben. Die Sache hat super geklappt, nur spätestens bei größeren Videos ist aufgefallen, dass die Sache schon sehr langsam und ruckelig ist. Aus diesem Grund habe ich gestern und heute etwas tiefer rein geschaut und untersucht, wo dort überhaupt die Performance verloren geht. Denn, ganz ehrlich, Performance darf auf meinem Rechner mit I7 und aktueller Radeon R9 kein Problem sein. Das Thema musste also irgendwo in meinem Coding liegen. Der Haupt-Performance-Fresser war dann mithilfe einer Leistungsanalyse in Visual Studio auch sehr schnell gefunden: Ein paar for-Schleifen.

Hier ein Beispiel für einen solchen Performance-Fresser:

unsafe
{
    int* mediaBufferPointerNative = (int*)mediaBufferPointer.ToPointer();
    int* targetBufferPointerNative = (int*)targetBuffer.Pointer.ToPointer();
    for (int loopY = 0; loopY < m_frameSize.Height; loopY++)
    {
        for (int loopX = 0; loopX < m_frameSize.Width; loopX++)
        {
            int actIndex = loopX + (loopY * m_frameSize.Width);
            targetBufferPointerNative[actIndex] = mediaBufferPointerNative[actIndex];
        }
    }
}

Was passiert ist ganz einfach: Die beiden for-Schleifen sorgen dafür, dass jeder Pixel (in X- und Z-Richtung) durchgeschliffen und schließlich von einem Speicherblock in den anderen kopiert wird. Bei einem Full-HD Video (1920×1080 Pixel) wären das dann insgesamt 2.073.600 Durchläufe. Jetzt denkt man sich als .Net Entwickler erst einmal nicht so viel dabei… schließlich muss man die Sachen ja irgendwie kopieren und wie soll man es sonst machen? Eine andere Alternative, welche ich vorher hatte, jedoch aufgrund von Kompatibilität zu WinRT wieder entfernen musste, war folgende:

NativeMethods.MemCopy(
    targetBuffer.Pointer,
    mediaBufferPointer,
    new UIntPtr((uint)(m_frameSize.Width * m_frameSize.Height * 4)));

... 

/// <summary>
/// Copies unmanaged memory from given source location to given target. 
/// </summary>
/// <param name="dest">The desitination address.</param>
/// <param name="src">The source address.</param>
/// <param name="count">Total count of bytes.</param>
[DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
public static extern IntPtr MemCopy(IntPtr dest, IntPtr src, UIntPtr count);

Ein neuer Versuch mit der vorherigen MemCopy Variante offenbarte dann, dass die Performance plötzlich deutlich schneller war. Warum? Im Nachhinein habe ich einen Blogeintrag von Alexandre Mutel [1] gefunden, welcher das Thema tiefer behandelt. Im Hintergrund passiert bei dem MemCopy scheinbar etwas völlig anderes, als in meinem naiven Ansatz. Neu für mich war nach dem Lesen des Artikels auch, dass es in .Net noch ein paar andere Möglichkeiten zum Kopieren von Speicher gibt, so etwa Buffer.BlockCopy. Definitiv ist dieser Blogeintrag von Alexandre das Lesen Wert, wenn man sich so wie ich aktuell mit Video-Processing beschäftigt, vor allem auch wegen dem Performance-Vergleich der verschiedenen Copy-Varianten.

Was bedeutet das jetzt für mich? Fürs erste setzte ich zumindest im Desktop-Build von Seeing# wieder auf MemCopy. Für WinRT muss ich mir aber noch eine andere Variante suchen, da die Verwendung von MemCopy im Windows Store nicht erlaubt ist. Die nächste Konsequenz wird sein, dass ich so oft wie möglich auf das Hin-und-Her-Kopieren solcher großer Speicherblöcke verzichte. Hier der aktuelle Stand der obigen Coding-Stelle:

MF.MediaBuffer mediaBuffer = mediaSharpManaged.GetBuffer();

int cbMaxLength;
int cbCurrentLenght;
IntPtr mediaBufferPointer = mediaBuffer.Lock(out cbMaxLength, out cbCurrentLengh);

#if DESKTOP
// Performance optimization using MemCopy
//  see http://code4k.blogspot.de/2010/10/high-performance-memcpy-gotchas-in-c.html
NativeMethods.MemCopy(
    targetBuffer.Pointer,
    mediaBufferPointer,
    new UIntPtr((uint)(m_frameSize.Width * m_frameSize.Height * 4)));
#else
// TODO: Search a more performant way on WinRT platform (MemCopy not allowed there)
unsafe
{
    int* mediaBufferPointerNative = (int*)mediaBufferPointer.ToPointer();
    int* targetBufferPointerNative = (int*)targetBuffer.Pointer.ToPointer();
    for (int loopY = 0; loopY < m_frameSize.Height; loopY++)
    {
        for (int loopX = 0; loopX < m_frameSize.Width; loopX++)
        {
            int actIndex = loopX + (loopY * m_frameSize.Width);
            targetBufferPointerNative[actIndex] = mediaBufferPointerNative[actIndex];
        }
    }
}
#endif

Verweise

  1. http://code4k.blogspot.de/2010/10/high-performance-memcpy-gotchas-in-c.html

2 Gedanken zu „Wie schnell man Speicher falsch kopieren kann“

  1. Im speziellen Fall liegt das daran, dass du 2 statt einer for-Schleife verwendest – und immer Berechnungen für den nächsten Index anstellen musst. Das Verhindert dann auch eine Reihe von Optimierungen. Wenn du nur eine Schleife verwenden würdest, könntest du dir die Berechnungen sparen und dem Compiler mehr Freiraum für Optimierungen geben.

    Es ist ein Irrglaube, dass Memcpy irgendwelche Performanceverbesserungen hält, wenn man durch die Verwendung von Pointern sowieso schon das Boundary Checking der CLR deaktiviert. Mehr Informationen findest du z.B. hier: http://nadeausoftware.com/articles/2012/05/c_c_tip_how_copy_memory_quickly

    Antworten
  2. Jap, völlig richtig, in den letzten Tagen habe ich auch etwas mehr mit den Sachen rumgespielt. Ein weiterer Punkt oben ist z. B. die Verwendung von int*. Wenn man anstelle von int mit long arbeitet, ist das Kopieren auch deutlich schneller.
    Wahnsinn eigentlich, was hier ein paar „Kleinigkeiten“ ausmachen. In LinqPad hab ich die Woche ein kleinen Test gebastelt, bei dem ich von 35 ms (schlechteste Methode) auf 2,5 ms runtergekommen bin.

    Mit dieser Methode hier bin ich praktisch an der Performance von der nativen MemCopy-Variante (Optimierung muss aktiviert sein):

    public static unsafe void CopyMemory(void* sourcePointer, void* targetPointer, ulong byteCount)
    {
    ulong longCount = byteCount / 8;
    ulong byteScrap = byteCount % 8;

    // Copy using long pointers
    ulong* sourcePointerLong = (ulong*)sourcePointer;
    ulong* targetPointerLong = (ulong*)targetPointer;
    for (ulong actIndexLong = 0; actIndexLong < longCount; actIndexLong++) { targetPointerLong[actIndexLong] = sourcePointerLong[actIndexLong]; } // Copy remaining bytes if (byteScrap > 0)
    {
    byte* sourcePointerByte = (byte*)sourcePointer;
    byte* targetPointerByte = (byte*)targetPointer;
    for (ulong actIndexByte = byteCount – byteScrap; actIndexByte < byteCount; actIndexByte++) { targetPointerByte[actIndexByte] = sourcePointerByte[actIndexByte]; } } }

    Antworten

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.