myCSharp.de - DIE C# und .NET Community
Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

» Hauptmenü
myCSharp.de
» Startseite
» Forum
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Suche
   » Plugin für Firefox
   » Plugin für IE7
   » Gadget für Vista
» Regeln
» Wie poste ich richtig?
» Datenschutzerklärung
» wbb-FAQ

Mitglieder
» Liste / Suche
» Stadt / Anleitung dazu
» Wer ist wo online?

Angebote
» ASP.NET Webspace
» Bücher
» Zeitschriften
   » dot.net magazin
» Accessoires

Ressourcen
» .NET-Glossar
» guide to C#
» openbook: Visual C#
» openbook: OO
» .NET BlogBook
» MSDN Webcasts
» dotnetjob.de
» Search.Net

Team
» Kontakt
» Übersicht
» Wir über uns
» Bankverbindung
» Impressum

» Unsere MiniCity
MiniCity
» myCSharp.de Diskussionsforum
Du befindest Dich hier: Community-Index » Diskussionsforum » Entwicklung » Basistechnologien und allgemeine .NET-Klassen » Lohnt sich Parallelisierung beim folgenden Algorithmus?
Letzter Beitrag | Erster ungelesener Beitrag Druckvorschau | An Freund senden | Thema zu Favoriten hinzufügen

Antwort erstellen
Zum Ende der Seite springen  

Lohnt sich Parallelisierung beim folgenden Algorithmus?

 
Autor
Beitrag « Vorheriges Thema | Nächstes Thema »
hawwk66
myCSharp.de-Mitglied

Dabei seit: 21.04.2012
Beiträge: 21


hawwk66 ist offline

Lohnt sich Parallelisierung beim folgenden Algorithmus?

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Hallo,

ich bin noch ein Newbie wenn es um parallele Programmierung geht; auch wenn ich mir die Basis-Theorie angeeignet habe.

Wäre dankbar, wenn ich etw. Feedback bekommen würde, ob es sich bei folgendem Algorithmus lohnt zu parallelisieren und wenn ja vor allem wie (richtig).

C#-Code:
public void PrepareMap ()
        {
            var myLock = new object ();

            for (int x = 0; x < WorldManager.Terrain.Width; x++)
            //Parallel.For (0, WorldManager.Terrain.Width, x =>
            {
                for (int y = 0; y < WorldManager.Terrain.Height; y++) {
                    var color = GetColorForHeight (x, y);

                    lock (myLock) {

                        ArrayList mapArray;

                        //check if an array needs to be created for the color
                        if (!MapArray.TryGetValue (color, out mapArray)) {
                            mapArray = new ArrayList ();
                            MapArray.Add (color, mapArray);
                        }

                        //add coordinates
                        mapArray.Add (x);
                        mapArray.Add (y);
                    }
                }
            }

public Color GetColorForHeight (int x, int y)
        {
            var value = WorldManager.Terrain.Heightmap [x * WorldManager.Terrain.ScaleFactor, y * WorldManager.Terrain.ScaleFactor];

            if (value > 0.8)
                return Color.Brown;
            else if (value > 0.5)
                return Color.Green;
            else
                return Color.Blue;

        }

Wie im Coding ersichtlich, habe ich als Kommentar die alternative Parallel.For Schleife eingefügt...zur Laufzeit merke ich keine (spürbare) Verbesserung...ich vermute es liegt an dem lock, da dieser der Flaschenhals ist.

Beide For-Loops werden jeweils 500x durchlaufen...

Hat jmd. Verbesserungsvorschläge?

Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von hawwk66 am 21.04.2012 16:46.

21.04.2012 16:23 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
Abt Abt ist männlich
myCSharp.de-Team (Moderation)

images/avatars/avatar-2981.png


Dabei seit: 20.07.2008
Beiträge: 3.379
Entwicklungsumgebung: VS2010, VS2012, ReSharper
Herkunft: Stuttgart


Abt ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Das ArrayList gehört wie alle untypisierten Collections in die Mottenkiste!

Nimm ne typisierte Collection - in diesem Fall am besten ein ConcurrentBag - und wirf das Lock raus. Und dann vergleich nochmal.

Theoretisch kann ein

C#-Code:
Parallel.ForEach(Enumerate.Range(0, WorldManager.Terrain.Width) , x =>
{
});

noch schneller sein; aber das muss man im Einzelfall prüfen.
21.04.2012 17:40 Beiträge des Benutzers | zu Buddylist hinzufügen
hawwk66
myCSharp.de-Mitglied

Dabei seit: 21.04.2012
Beiträge: 21

Themenstarter Thema begonnen von hawwk66

hawwk66 ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

ok, erst mal danke für dein Hw...an die concurrentCollections hab ich nicht mehr gedacht...

ich habe es jetzt folgendermassen umgestellt:

C#-Code:
            //for (int x = 0; x < WorldManager.Terrain.Width; x++)                     
            Parallel.ForEach (Enumerable.Range(0, WorldManager.Terrain.Width), x =>
            {
                for (int y = 0; y < WorldManager.Terrain.Height; y++) {

                    var color = GetColorForHeight (x, y);

                    ConcurrentBag<float> mapList;
                    mapList = MapDictionary.GetOrAdd (color, new ConcurrentBag<float> ());

                    //add coordinates
                    mapList.Add (x);
                    mapList.Add (y);

                    //}
                }
            }
            );

Leider dauert es jetzt wesentlich länger (mindestens Faktor 3)...funktional läuft es aber korrekt. Hast Du eine Idee (schlechte Performance) warum ? Ich benutze nicht .NET 4, sondern Mono (für Android, Runtime läuft auf einem QuadCore TabletPC)...wenn Du der Meinung bist, das müsste schneller laufen, als die nicht-parallelisierte Version würde ich das ganze mal testweise in ein .NET 4 Projekt packen und auf einem Intel Quadcore lauffen lassen...
21.04.2012 19:32 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
Abt Abt ist männlich
myCSharp.de-Team (Moderation)

images/avatars/avatar-2981.png


Dabei seit: 20.07.2008
Beiträge: 3.379
Entwicklungsumgebung: VS2010, VS2012, ReSharper
Herkunft: Stuttgart


Abt ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Nur weil man Parallel arbeit heißt das nicht, dass das schneller ist.
Du hast auch einen Overhead; hab hier im Forum schon einige Beispiele gezeigt, dass parallele Tasks auch deutlich langsamer sein können - vor allem, wenn es sicht bezüglich der Berechnung einfach nicht lohnt.

Ich hab nie gesagt, dass meine Lösung schneller ist; sie ist nur sauberer als mit locks.
Keine Ahnung, was WorldManager.Terrain.Heightmap ist oder tut.

Fiktives Beispiel:

Bei "einfachen" Dingen, wird die parallele Lösung langsamer sein. zB
var result = Enumerable.Range(0, 100000).Sum(x=> x*x);
statt
var result = Enumerable.Range(0, 100000).AsParallel().Sum(x=> x*x);

Mit ziemlich hoher Wahrscheinlichkeit ist
var result = Enumerable.Range(0, 100000).Sum(x=> x*x*x*x*x*x*x*x*x*x);
als parallele Lösung schneller:
var result = Enumerable.Range(0, 100000).AsParallel().Sum(x=> *x*x*x*x*x*x*x*x*x);

Wobei optimiert mit der Math-Class könnte auch wieder die sequenzielle Lösung schneller sein. Man sollte nicht dort versuchen zu optimieren, wenn man einen Hotspot erahnt, sondern nur dort, wo auch wirklich einer ist.
21.04.2012 20:03 Beiträge des Benutzers | zu Buddylist hinzufügen
DerKleineTomy
myCSharp.de-Mitglied

Dabei seit: 17.02.2012
Beiträge: 74
Entwicklungsumgebung: Visual Studio 2010


DerKleineTomy ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Versuch mal die beiden For-Schleifen zu vertauschen, also erst y, dann x. Wenn WorldManager.Terrain.Heightmap ein zweidimensionales Array ist, dann sollte es zu einer Verbesserung kommen (weniger Sprünge beim Speicherzugriff).
21.04.2012 23:31 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
herbivore
myCSharp.de-Team (Admin)

images/avatars/avatar-2627.gif


Dabei seit: 11.01.2005
Beiträge: 47.478
Entwicklungsumgebung: csc/nmake (nothing is faster)
Herkunft: Berlin


herbivore ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Hallo DerKleineTomy,

im Vergleich zur Bremswirkung durch lock kann man die Beschleunigung durch "burst-fähige" Speicherzugriffe komplett vernachlässigen.


Hallo hawwk66,

insofern muss das Ziel sein, das lock ganz zu vermeiden. Lesende Zugriffe auf Daten, die während der Abarbeitung nicht verändert werden, müssen sowieso nicht synchronisiert werden. Bleibt die Sammlung der Ergebnisse in mapArray und MapArray.

Nun weiß ich nicht, wie ConcurrentBag intern arbeitet. Wenn es lock-frei arbeitet, ist es gut. Wenn nicht, kannst du es mal mit  Lockfreie threadsichere Queue statt ConcurrentBag versuchen.

Allerdings müssen auch alle (und leider nicht nur die schreibenden) Zugriffe auf MapArray synchronisiert werden. Da müsstest du mal schauen, ob du eine thread-sichere, aber lock-freie Implementierung eines Dictionary findest.

Erst wenn du das lock komplett eliminieren kannst, besteht die Chance, dass die parallele Implementierung schneller ist, als eine sequentielle.

herbivore
22.04.2012 09:57 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
hawwk66
myCSharp.de-Mitglied

Dabei seit: 21.04.2012
Beiträge: 21

Themenstarter Thema begonnen von hawwk66

hawwk66 ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

@Abt: Ist mir schon klar, dass Parallelisierung nicht immer schneller ist; wollte es einfach mal ausprobieren, weil die Performance an der Stelle kritisch ist (Thread, welcher eine Karte auf ein Canvas rendert).

@herbivore
danke für den Tipp.


Ich werde aber die Parallisierung hier jetzt mal sein lassen...ich habe nicht den Eindruck, dass es an dieser Stelle viel bringt...bzw. eher schadet.

Vermutlich muss ich später das Zeichnen der Karte komplett ändern, d.h, dass ich nicht einzelne Punkte sondern gleiche Bereiche hinsichtlich der Höhe(=Color) betrachte und so die Anzahl der Schleifendurchläufe verringern kann.
22.04.2012 10:28 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
DerKleineTomy
myCSharp.de-Mitglied

Dabei seit: 17.02.2012
Beiträge: 74
Entwicklungsumgebung: Visual Studio 2010


DerKleineTomy ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Zitat von herbivore:
im Vergleich zur Bremswirkung durch lock kann man die Beschleunigung durch "burst-fähige" Speicherzugriffe komplett vernachlässigen.

Ich hab mich wohl nicht ganz klar ausgedrückt. Ich beziehe mich auf das Konstrukt bei dem nicht parallelisiert wird, sondern nur die normalen Schleifen benutzt werden. Da sollte das Lock dann ja entfallen. Ich habe letztens erst durch das Vertauschen den Zugriff auf ein 500x500 Array um 25% beschleunigt.

Noch als Hinweis: Die Geschwindigkeit solltest du auf jedenfall im Releasemodus testen (wenn du es nicht schon tust), denn da verschwinden die Probleme manchmal von ganz allein :)
22.04.2012 12:39 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
weismat
myCSharp.de-Mitglied

Dabei seit: 20.09.2005
Beiträge: 448
Entwicklungsumgebung: Vistual Studio 2010 Resharper
Herkunft: Frankfurt am Main


weismat ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Dein lock muesste meines Erachtens nur um MapArray.Add (color, mapArray); stehen.
Ob sich Parallelisierung lohnt, haengt davon ab, wie lange WorldManager.Terrain.Heightmap [x * WorldManager.Terrain.ScaleFactor, y * WorldManager.Terrain.ScaleFactor] braucht - der Rest ist ja sehr schnell abgearbeitet..
23.04.2012 13:57 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
herbivore
myCSharp.de-Team (Admin)

images/avatars/avatar-2627.gif


Dabei seit: 11.01.2005
Beiträge: 47.478
Entwicklungsumgebung: csc/nmake (nothing is faster)
Herkunft: Berlin


herbivore ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Hallo weismat,

Zitat:
Dein lock muesste meines Erachtens nur um MapArray.Add (color, mapArray); stehen.

nein, wie ich schon weiter oben schrieb, muss das lock nicht nur um die schreibenden Zugriffe auf MapArray, sondern auch um die lesenden, denn sonst kann es sein, dass das Lesen während einer Änderung inkonsistente Ergebnisse liefert. Außerdem muss das lock auch die Zugriffe auf mapArray umschließen, denn auch darauf können verschiedene Threads gleichzeitig zugreifen.

Wenn man das lock so umfassend macht (machen muss), dann ist wird es doch wieder bei jedem Schleifendurchlauf durchgeführt und damit vermutlich zu aufwändig. Deshalb auch mein Rat mit den lock-freien Zugriffen.

herbivore
23.04.2012 14:03 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
unconnected unconnected ist männlich
myCSharp.de-Mitglied

images/avatars/avatar-3200.jpg


Dabei seit: 13.08.2006
Beiträge: 601
Entwicklungsumgebung: VS2012 Ultimate
Herkunft: Oerlinghausen/NRW


unconnected ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Vllt solltest Du nicht versuchen 100000 einzelne Threads zu starten (bzw die laufenden Threads immer nur mit einer Rechenaufgabe zu befüllen).

Versuch einmal vllt nur 4 oder 8 Threads aufzumachen. Denen Du dann jeweils nur 1/4 oder 1/8 der gesamten Aufgabe gibst.

Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von unconnected am 25.04.2012 16:16.

25.04.2012 16:16 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
MrSparkle MrSparkle ist männlich
myCSharp.de-Poweruser/ Experte

images/avatars/avatar-2159.gif


Dabei seit: 16.05.2006
Beiträge: 2.992
Entwicklungsumgebung: VS.NET 2010, ReSharper
Herkunft: Leipzig


MrSparkle ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Zitat von unconnected:
Versuch einmal vllt nur 4 oder 8 Threads aufzumachen. Denen Du dann jeweils nur 1/4 oder 1/8 der gesamten Aufgabe gibst.

Ganz genau, wobei es optimalerweise so viele Threads gibt, wie man Prozessorkerne auf dem System hat. Das ganze nennt sich Partitioning und wird hier beschrieben:  Partitioning mit Parallel.For.

Christian
25.04.2012 17:01 Beiträge des Benutzers | zu Buddylist hinzufügen
zommi zommi ist männlich
myCSharp.de-Poweruser/ Experte

images/avatars/avatar-2617.png


Dabei seit: 14.11.2007
Beiträge: 1.229
Entwicklungsumgebung: VS 2005+2010
Herkunft: Berlin


zommi ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Hey,

Zitat von MrSparkle:
Das ganze nennt sich Partitioning[...]

Eigentlich nennt sich das Mapping (nach  Foster's Methodology) Die (Daten-)Partitionierung des Problems hast du schon vorgenommen, wenn du Parallel.For bereits hinschreibst, weil du schon den Algo für das Gesamtproblem in einzelne Tasks für jedes Element aufgebrochen hast.

Zitat von unconnected:
Versuch einmal vllt nur 4 oder 8 Threads aufzumachen.

Agglomeration und Mapping selbst übernimmt (wenn nicht überschrieben) das .NET Framework von allein, in sofern wird schon eine 'optimale' Anzahl an Threads verwendet.

Aber zurück zu hawwk66's eigentlichem Anliegen, seinen konkreten Algorithmus zu beschleunigen.

Zitat von hawwk66:
[lohnt] es sich bei folgendem Algorithmus [...] zu parallelisieren und wenn ja vor allem wie (richtig).

Unabhängig von der Parallelisierung würde ich zunächst Folgendes ändern:
  • Du hast nur 3 (konstante!) Farben. Also besteht dein MapArray-Dictionary nur aus drei Einträgen. Jeder Zugriff kostet aber unnötig Zeit. Also eliminiere den! Statt dass GetColorForHeight eine Farbe zurückgibt, mit der du im MapArray nach der passenden Liste nachschlagen kannst, lass dir direkt die passende Liste zurückgeben! So sparst du eine viertel Million Dictionary-Zugriffe.

    Mach 1-Mal einen Dictionay-Zugriff

    C#-Code:
    List<int> brownList = MapArray[Color.Brown];
    List<int> greenList = MapArray[Color.Green];
    List<int> blueList = MapArray[Color.Blue];

    Und lass dir dann immer direkt die richtige Listige zurückgeben:

    C#-Code:
    public Color GetListForHeight (int x, int y, List<int> brownList, List<int> greenList, List<int> blueList)
    {
        var value = WorldManager.Terrain.Heightmap [x * WorldManager.Terrain.ScaleFactor, y * WorldManager.Terrain.ScaleFactor];

        if (value > 0.8)
            return brownList;
        else if (value > 0.5)
            return greenList;
        else
            return blueList;
    }

  • Auch ich empfehle Jagged statt Rectangular Arrays mit einhergehender Vertauschung der Schleifen-Variablen. Also float[y][x] statt float[x,y].
    (Hier ist es bei Bildern in jagged-Arrays üblich, zeilenweise vorzugehen, daher float[y][x])
    Und dann Y für die äußere und X als innere Schleife verwenden. So gibt es in der inneren Schleife weniger Cache-Misses beim Iterieren, da ja alle x-Werte hintereinander im Speicher liegen.

    Ich stimme zwar herbivore zu:

    Zitat von herbivore:
    im Vergleich zur Bremswirkung durch lock kann man die Beschleunigung durch "burst-fähige" Speicherzugriffe komplett vernachlässigen.

    Aber wenn das Locking erstmal weg ist (beispielsweise weil weiterhin sequentiell), dann merkt man auch hier den Unterschied!
Das beides zusammen gibt einen Speedup von fast 3x.

Nun zum interessanten Part: Parallelisierung

Wie schon mehrfach aufgezeigt, ist das locking hier eines der Probleme. Aber man kann noch einen Schritt weiter gehen und das eigentliche Problem beim Verwenden einer gemeinsam genutzten Datenstruktur selbst sehen. Dies ist für jede parallele Abarbeitung 'tödlich'. Daher sollte - wenn du es parallel abarbeiten willst - jeder Thread die Daten unabhängig von den anderen, separat in privaten Listen sammeln.
Keine gemeinsam genutzten Datenstrukturen, kein Problem :-)

... Naja... leider nur die halbe Wahrheit. Denn du willst ja am Ende alle doch wieder in einer gemeinsamen großen Liste haben. Daher muss der parallelen Abarbeitung noch ein Schritt folgen, der die Ergebnisse der einzelnen Threads zu einem großen Ergebnis zusammenfügt.

Kurz gefasst: Spendiere jedem Thread ein privates 'MapArray' und wirf am Ende alle in ein gemeinsames. Allerdings wird hier das 'Zusammenführen' dann wieder zum Flaschenhals, der mitunter jeglichen Speedup wieder zunichte machen kann. Und genau das passiert auch!

Ich hab es mal testweise implementiert und der zusätzliche Speedup der parallelen Lösung ist sehr gering (wenige %). Gerade im Vergleich zu den obigen vorherigen Optimierungen. Es ist zwar schneller, aber nicht wesentlich. (auf einem Q9300 QuadCore) Das Zusammenführen frisst also den Speedup wirklich komplett auf.

Hier mein Snippet:

C#-Code:
using ColorMap = System.Collections.Generic.Dictionary<System.Drawing.Color, System.Collections.Generic.List<int>>;

private ColorMap InitializeThreadLocalMapArray()
{
    var threadLocalMapArray = new ColorMap();
    threadLocalMapArray[Color.Brown] = new List<int>();
    threadLocalMapArray[Color.Green] = new List<int>();
    threadLocalMapArray[Color.Blue] = new List<int>();
    return threadLocalMapArray;
}

private ColorMap PrepareMapFromTerrainSlice(int y, ParallelLoopState state, ColorMap threadLocalMapArray)
{
    var brown = threadLocalMapArray[Color.Brown];
    var green = threadLocalMapArray[Color.Green];
    var blue = threadLocalMapArray[Color.Blue];

    for (int x = 0; x < WorldManager.Terrain.Width; x++)
    {
        var mapArray = GetColorListForHeight(x, y, brown, green, blue);
        //add coordinates
        mapArray.Add(x);
        mapArray.Add(y);
    }
    return threadLocalMapArray;
}

private void JoinAllMapArrays(ColorMap threadLocalMapArray)
{
    lock (MapArray)
    {
        foreach (var color in threadLocalMapArray.Keys)
        {
            MapArray[color].AddRange(threadLocalMapArray[color]);
        }
    }
}

override public void PrepareMap()
{
    MapArray[Color.Brown] = new List<int>();
    MapArray[Color.Green] = new List<int>();
    MapArray[Color.Blue] = new List<int>();

    Parallel.For<ColorMap>(
        0, WorldManager.Terrain.Height,
        InitializeThreadLocalMapArray,
        PrepareMapFromTerrainSlice,
        JoinAllMapArrays
    );
}

protected List<int> GetColorListForHeight(int x, int y, List<int> brown, List<int> green, List<int> blue)
{
    var value = WorldManager.Terrain.Heightmap[y][x];

    if (value > 0.8)
        return brown;
    else if (value > 0.5)
        return green;
    else
        return blue;
}

Wie man sieht, nutze ich hier eine spezielle Überladung von Parallel.For, wo ich für jeden Thread noch etwas vorbereiten kann (das private Dictionary) und am Ende auch noch etwas Arbeit (das Zusammenführen) durchführen kann.

Bei 500x500 Maps geht fast 50% der Zeit auf das Zusammenführen in der JoinAllMapArrays-Methode.

Das ist nun nicht verwunderlich, da das AddRange fleißig alle Listen umherkopiert.
Optimieren lässt sich das nur noch, wenn du auf das Kopieren verzichtest. Also die Ergebnisse in den privaten Listen belässt und nur 'virtuelle' Gesamtlisten erstellst, die intern immernoch aus den Teillisten aufgebaut ist, statt einem riesigen Array.

Beispielsweise mit IEnumerable.Concat statt List.AddRange.
Da musst du aber schauen, ob es dir nach außen reicht, wenn dein MapArray nur IEnumerables hält.

Wenn das okay wäre, dann änder MapArray von Dictionary<Color, List<int>> auf Dictionary<Color, IEnumerable<int>> und verwende beim Zusammenführen die  Concat Methode.

C#-Code:
MapArray[color] = MapArray[color].Concat(threadLocalMapArray[color]);

Dann kostet das so gut wie keine Zeit mehr und die kannst die Parallelisierung genießen. Ich komme bei mir damit nochmal auf einen Faktor zwischen 1.5x und 2x.

beste Grüße
zommi

Dieser Beitrag wurde 4 mal editiert, zum letzten Mal von zommi am 25.04.2012 19:10.

25.04.2012 19:03 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
Baumstruktur | Brettstruktur       | Top 
myCSharp.de | Forum Der Startbeitrag ist älter als ein Jahr.
Der letzte Beitrag ist älter als ein Jahr.
Antwort erstellen


© Copyright 2003-2013 myCSharp.de-Team. Alle Rechte vorbehalten. 20.05.2013 21:44