Source Code mit Nutzung con Com-Objejten deutlich langsmer im Thread

  • VB.NET
  • .NET (FX) 4.0

Es gibt 96 Antworten in diesem Thema. Der letzte Beitrag () ist von Tukuan.

    @ RodFromGermany: So geht's nun. Danke.

    VB.NET-Quellcode

    1. Private Sub MultiplyMatricesParallel2(ByVal matA As Double(,), ByVal matB As Double(,), ByVal result As Double(,))
    2. Dim matARows As Integer = matA.GetLength(0)
    3. ' A basic matrix multiplication.
    4. ' Parallelize the outer loop to partition the source array by rows.
    5. Parallel.For(0, matARows, Sub(i)
    6. MultMatrix(i, matA, matB, result)
    7. End Sub)
    8. End Sub
    9. Sub MultMatrix(ByVal i As Integer, ByVal matA As Double(,), ByVal matB As Double(,), ByVal result As Double(,))
    10. Dim matACols As Integer = matA.GetLength(1)
    11. Dim matBCols As Integer = matB.GetLength(1)
    12. For j As Integer = 0 To matBCols - 1
    13. ' Use a temporary to improve parallel performance.
    14. Dim temp As Double = 0
    15. For k As Integer = 0 To matACols - 1
    16. temp += matA(i, k) * matB(k, j)
    17. Next
    18. result(i, j) += temp
    19. Next
    20. End Sub



    ErfinderDesRades schrieb:

    Das ist Unfug.
    Ich denke, herausgearbeitet ist inzwischen, dasses nicht der Gui-Zugriff ist, der laggt, sondern dass iwelche Daten auf Platte geschrieben werden.
    In deim gezeigten Code finden aber überhaupt keine Schreibzugriffe statt - wie gesagt: du suchst an der falschen Stelle.

    Da magst du recht haben.
    Allerdings wird nicht parrallel auf die Platte geschrieben. Zurzeit erstellt der Thread eine Datei, wenn alles fertig gerechnet ist. Nur der Thread (nicht die GUI) greift auf die Platte zu. Ich werde mal schauen, was wie lange dauert.
    Aber es ist mir immer noch schleierhaft warum das Berechnen und Schreiben aus dem GUI-Thread 2 min und in einem anderen Thread 8 min dauert. Es dürfte doch eigentlich egal sein.

    ErfinderDesRades schrieb:

    Geschwindigkeit gebracht?
    Vom Hinsehen würde ich sagen, dass das genau mit der Anzahl der benutzten Kerne geht (entgegengesetzt proportional).
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Ja, aber das ist das olle Optimierungs-Phänomen: Du kannst die Berechnungs-Geschwindigkeit mw. vertausendfachen, es bringt nix, wenn die Performance am Schreiben der Daten hängt.
    Also angenommen, die Berechnung benötigt normal 0.5s, und das Datenschreiben 50s.
    Da bringt die tausendfache Berechnungs-Geschwindigkeit eine letztendliche Verbesserung von 50.5s -> 50.0005s
    Also knapp 1%, nicht wahrnehmbar - Unfug.

    Google mal "rules of optimization"
    Korrekt, ich bezog mich auf den blanken geposteten Parallel-Code.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Hab gerade mal ein paar Tests gemacht. Zunächst mit dem Beispiel von MSDN, welches oben als Source Code auch schon beschrieben ist.
    Berechnet man die Matrizen nicht parallel dauert es ca. 2000 ms.
    Berechnet man sie Parallel mit lambda expression geht es mit 650 ms wesentlich schneller.
    Die 2. Parallel-Verion mit dem Aufruf dauert aber wieder fast doppelt so lange....
    Executing sequential loop...
    Sequential loop time in milliseconds: 2072
    Executing parallel loop...
    Parallel loop number (lambda expression): time in milliseconds: 658
    Parallel loop number (named methode): time in milliseconds: 1221
    Press any key to exit.

    Habt ihr eine Idee, warum es mit Lambda schneller ist?


    Und dann hab ich noch die Zeiten für die eigentliche Berechnung (warum ich diesen Thread gestartet habe):
    jeweils in min:s
    Berechnung
    Speichern als Datei
    Gesammt

    aus der GUI
    1:53
    0:03
    1:56

    extra Thread
    6:06
    0:31
    6:37


    Aber hier ist noch keine Parallelisierung drin. Da muss ich wohl noch einiges umbauen.

    Noch mal die Frage: Warum ist die Berechnung aus der GUI so viel schneller?
    Ich verstehe das nicht so richtig.
    Na, wenn die Rechnungen langsamer sind als das Wegschreiben, dann mussich den Unfug wohl zurücknehmen.

    Zur Frage: Der Gui-Thread hat glaub sehr hohe Priorität, je nachdem, wie das Threading aussieht kann der Nebenthread ziemlich nachrangig behandelt werden.

    Warum Lambdas signifikant schneller sind als benannte Methoden - k.A.
    Ich tippe drauf, dass iwie anners und besser gecodet.

    Müsste ich jetzt den direkten Vergleich sehen zw. der Lambda-Verwendung und der mitte benannten Methode.
    OK, danke. Heißt dann aber soviel wie:
    1. Code umschreiben und parallelisieren und
    2. im GUI-Thread laufen lassen, damit er eine höhere Priorität hat.
    Damit man dann noch auf die GUI zugreifen kann, fügt man ab und an ein DoEvents ein? Ich dachte, dass macht man auf keinen Fall mehr...

    Tukuan schrieb:

    im GUI-Thread laufen lassen
    bedeutet, die GUI auszubremsen.

    RodFromGermany schrieb:

    In einem separaten Thread machst Du Parallel.For.
    Diesem Thread kannst Du die entsprechende Priorität geben.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    Tukuan schrieb:

    Kann mir einer dazu sonst noch was sagen:
    Beim Umgang mit COM Objekten in .NET:
    ⇨ Geeignetes Threading Modell wählen.
    ⇨ Zwischen deterministischem und undeterministischem (GC) Löschen der Objekte wählen.
    ⇨ Nachrichten pumpen

    Was bedeutet das?


    Bisher ist noch niemand auf diese (sehr wichtige!) Frage eingegangen.

    Es gibt zwei hauptsächlich verwendete Threading-Modelle in COM. Das eine heißt "Apartment-Threaded", das andere "Free-Threaded" (das dritte heißt "Both"). Das Modell legt fest, in welchen sogenannten Apartments Threads ausgeführt werden können, sogenannte Single-Threaded- oder Multi-Threaded-Apartments (STA/MTA). Wichtig sind folgende Eckpunkte:
    - Eine VB.NET-Anwendung läuft in einem STA, weil Windows Forms nicht in einem MTA funktioniert (oder nur eingeschränkt, Beispiel OpenFileDialog oder allgemein OLE)
    - Damit gilt auch: VB.NET-Anwendungen sind Apartment-Threaded
    - Neu erstellte Threads werden in einem MTA erstellt

    Das führt uns zu Problem 1: Wenn Aufrufe von einem STA (deine GUI) in ein MTA (Workerthread) stattfinden, müssen Daten gemarshallt werden. Das bedeutet Kopieren & Pinnen von .NET --> COM und nochmal Kopieren von STA --> MTA (und 2x zurück). Heißt: 4 Kopiervorgänge pro Aufruf - das dauert. Läuft die COM-Komponente im GUI-Thread, sparst du die letzte Kopie, musst aber synchron auf das Ergebnis warten, sodass die GUI hängt.

    Problem 2 ist nicht weit: Der GC (Garbage Collector) sammelt .NET-Objekte irgendwann ein (=nichtdeterministisch), COM arbeitet aber mit Referenzzählung (=deterministisch). Das ist problematisch, weil der GC nicht wissen kann, wann ein COM-Objekt entfernt werden kann. Er wartet also, bis der sogenannte RCW das COM-Objekt freigegeben hat. Je nachdem, wie viele Instanzen du von der Komponente erstellst, verschwendest du also ggf. Speicher. Ganz schlecht wäre es, wenn du die Komponente in einer Schleife für jede Berechnung neu instanzierst. Das kostet sehr viel Performance.

    Nachrichten pumpen: Läuft die Komponente im GUI-Thread, bekommst du diesen Punkt geschenkt, weil jedes Fenster bereits eine Nachrichtenschleife (auch "message pump", deshalb "pumpen") besitzt. In einem externen Thread ist das nicht der Fall - Kommunikation zwischen Threads wird aber über WindowMessages abgewickelt. Da diese ohne Nachrichtenschleife nicht abgeholt werden, läuft die Queue des Ziel-Threads ggf. voll und bei standardmäßig 10000 Nachrichten fliegt eine Exception --> Crash.

    Die Einhaltung der 3 Punkte, die im Zitat genannt werden, sollte also für die nötige Performance sorgen (was jedoch einiges an Hintergrundwissen erfordert).

    Beispiel:
    Falsch (Komponente nicht im gleichen Thread erstellt, aus dem sie aufgerufen wird):

    Visual Basic-Quellcode

    1. Dim c as New ComObject()
    2. Thread.Start(ThreadProc)
    3. Sub ThreadProc()
    4. c.DoSomething()
    5. End Sub


    Richtig:

    Visual Basic-Quellcode

    1. Thread.Start(ThreadProc)
    2. Sub ThreadProc()
    3. Dim c As New ComObject()
    4. c.DoSomething()
    5. End Sub


    Lesestoff:
    msdn.microsoft.com/en-us/library/ms809971.aspx
    support.microsoft.com/kb/150777/en-us
    Gruß
    hal2000
    Danke für die Nachricht, hal2000. Auch wenn ich noch nicht genau weiß, was es für mich bedeutet. Ist halt komplett neu für mich. WErde mich dann mal an den Lesestoff machen.
    Immerhin ist der Sourcecode "Richtig" gecodet, wenn ich deine beiden Beispiele anschaue.

    Es muss aber nicht viel zwischen GUI und Thread kopiert werden:
    Der Thread bekommt eigentlich nur gesagt, wo die Rohdaten liegen und wo er die Ergebnisdaten hinschreiben soll. Dazu kommen noch ein paar Flags. Der Thread besorgt sich dann die Daten. Diese sind dann aber ein wenig größer. Ist das vielleicht ein Problem? Eine Rohdaten Datei ist immerhin ca. 560 MB groß.

    RodFromGermany schrieb:

    bedeutet, die GUI auszubremsen.

    Tja, was solls. GUI ausbremsen ist doof. Aber ich muss die Daten in 10 min abarbeiten. Wenn ich es im Thread nicht schnell hinbekomme, muss sich der User halt notfalls gedulden. Immerhin läuft das Programm zu 99 % autonom.

    Ich werde mal versuchen den Sourcecode ein wenig zu optimieren. Das mit dem Parallel.For ist bestimmt ne Prima Sache.

    Tukuan schrieb:

    GUI ausbremsen ist doof.
    Vielleicht solltest Du in einem kleinen Testprogramm das Auslagern von Berechnungen und Parallel.For in einen separaten Thread üben, und wenn Du das verstanden hast, packst Du in den Rechenthread Deine COM-Komponente dazu. Aber erst dann.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    ich find die Aufgabe ja sehr reizvoll.
    Imo könntest du ein klein Testprojekt basteln, 1MB Daten dazu, zippen und anhängen, vlt sogar auf [Sammelthread] Knobel-Aufgaben, knifflige Algorithmen, elegante Lösungen
    Mit der Aufgabe, aus der Daten-Datei möglichst schnell eine Ergebnis-Datei zu produzieren.
    Das wäre im Grunde sogar einen kleinen Tipp wert, denn was mit Parallel.For haben wir noch nicht.
    Vlt. stellt sich auch heraus, dasses bereits optimal ist, also dass kann ich mir doch vorstellen, dass das Durchmultiplizieren von 560MB Matritzen bisserl was dauert.
    In deinem Testprojekt müsste deine Lösung bereits drinne sein, damit man das eigene Ergebnis gegenprüfen kann.

    Was ich die ganze Zeit noch nicht verstehe, wo in deinem Problem die Com-Componente vorkommt. Wenn ich recht verstanden habe, geht es doch um eine Daten-Datei, wo Matritzen drinne sind, die auszumultiplizieren sind?

    ErfinderDesRades schrieb:

    ich find die Aufgabe ja sehr reizvoll.

    Eine nette Idee. Aber ich darf die Com-Schnittstelle nicht raus geben (ist ein Lizenz Ding) und ohne die geht es nunmal gar nicht.

    ErfinderDesRades schrieb:

    Was ich die ganze Zeit noch nicht verstehe, wo in deinem Problem die Com-Componente vorkommt. Wenn ich recht verstanden habe, geht es doch um eine Daten-Datei, wo Matritzen drinne sind, die auszumultiplizieren sind?

    Da ist wohl was durcheinander gekommen.
    Das mit den ausmultiplizieren von Matrizen habe ich nur reingebracht, weil mit nicht ganz klar war, wie das mit dem Parallelisieren funktioniert. Hätte wohl einen extra Thread starten sollen.
    Dis eigentliche Aufgabe ist mit Hilfe der Com-Schittstelle, Rohdaten auszulesen, von Messkanälen FFT Analysen durchzuführen und diese zu sortieren bzw. Analysen damit zu machen und die Ergebnisse dann abzuspeichern.
    Der Zugriff auf die Rohdateien, die Ergebnissdateien sowie die gesammte Berechnung geschieht über die Com-Schittstelle von imc / Famos.

    Ich werde mich heute daran machen den Sourcecode von meinem Kollegen zu durchwühlen und zu optimieren. Vielleicht poste ist das Ergebnis dann mal. Aber wie gesagt: ohne die com-Schittstelle, die halt sehr speziell ist, wird es euch nichts bringen.

    Tukuan schrieb:

    FFT Analysen durchzuführen
    Kannst Du das mal etwas ausführlicher darlegen, Aufgabe und Umsetzung?
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    So sieht der SourceCode zurzeit aus, dem ich vom Kollegen umgebaut habe. Ich habe noch keine Paralelisierung eingearbeitet und es fehlt noch das Berechnen der statistischen Werte (min, max, mean, RMS) sowie das speichern:

    VB.NET-Quellcode

    1. Public Sub CalculateHarmonics()
    2. If Not IO.File.Exists(SourceFile) Then
    3. RaiseEvent ErrorOccur(True, SourceFile & " not found")
    4. Exit Sub
    5. End If
    6. Dim stopwatch As New Stopwatch()
    7. stopwatch.Start()
    8. Console.WriteLine(stopwatch.ElapsedMilliseconds & " - Start")
    9. 'FamosObjekt
    10. Dim MyFamos As New ImcFamosLib.Famos
    11. MyFamos.ResultDataFormat = ImcFamosLib.FsResultFormatConstants.cfsFormatDouble
    12. MyFamos.Spec.FFTMode = ImcFamosLib.FsFFTModeConstants.cfsModeAppend
    13. MyFamos.Spec.FFTWindow = ImcFamosLib.FsFFTWindowConstants.cfsWinRectangle
    14. Dim famosFile As New ImcCoreLib.DFile
    15. Dim window As Integer = CInt(myProjectData.GridFrequency * 0.2)
    16. For i = 0 To channelNames.Length - 1
    17. Dim raw As ImcCoreLib.DChannel
    18. famosFile.Open(_SourceFile, ImcCoreLib.DmFileOptionConstants.cdmFileQuickLoad)
    19. raw = GetFamosChannelFromFile(famosFile, channelNames(i), False)
    20. famosFile.Close()
    21. Console.WriteLine(stopwatch.ElapsedMilliseconds & " - channel read")
    22. If IsNothing(raw) Then
    23. RaiseEvent ErrorOccur(False, "Channel " & channelNames(i) & " not found")
    24. Else
    25. raw.TransferToFamos()
    26. Dim samples As Integer = 8192
    27. If raw.xDelta = 0.00005 Then
    28. 'samples rate 20000
    29. samples = 4096
    30. End If
    31. 'Filter parameter: Butterworth,-,8th order, cut of freq. = 100 Hz
    32. Dim rawResample As ImcCoreLib.DChannel = MyFamos.Filter.LowPass(raw, 0, 0, 8, 100)
    33. rawResample.Name = raw.Name & "_lowpass"
    34. rawResample.TransferToFamos()
    35. 'Calculate the zero points; returns 'zero points' for 10 periods
    36. Dim rawZeros As ImcCoreLib.DChannel = CalculateZeroCrossings(raw, rawResample, window)
    37. rawZeros.Name = "Zeros"
    38. rawZeros.TransferToFamos()
    39. Console.WriteLine(stopwatch.ElapsedMilliseconds & " - zeros calc.")
    40. Dim part As ImcCoreLib.DChannel
    41. Dim spec As ImcCoreLib.DChannel
    42. For j = 1 To 5 'rawZeros.Length - 1
    43. part = MyFamos.Edit.CopyPartX(raw, rawZeros.Value(j), rawZeros.Value(j + 1))
    44. part.Name = raw.Name & "_" & j
    45. part.TransferToFamos()
    46. 'part.xOffset = 0
    47. spec = CalculateFFT(MyFamos, part, (rawZeros.Value(j + 1) - rawZeros.Value(j)), samples)
    48. spec.Name = raw.Name & "_spec_" & j
    49. spec.TransferToFamos()
    50. 'TODO: work with spec(trum): calc/check min, max mean, RMS values
    51. Console.WriteLine(stopwatch.ElapsedMilliseconds & " - loop " & j)
    52. Next
    53. End If
    54. Next
    55. End Sub


    Zur imc-Com-Schnittstelle gehören (hoffe ich hab nichts vergessen):
    • imccorelib
    • ImcFamosLib
    Es sind noch einige Sachen drin, die im nachhinein angepaßt (Stopwatch usw. raus) werden.
    Ist das FFT-Spektrum berechnet, werden Minimum, Maximum, Mittelwert (Summe) und RMS (Quadratische Summe) gebildet bzw. ermittelt. Ein Spektrum hat ca. 2x1800 Double Werte. Von 1800 Werten brauchen wir die 4 Parameter. Diese Werte werden dann gespeichert.

    Das auslesen Channels und speichern das speichern der Ergebnisse dauert nich lange. Hier mal die Werte bis zum ersten Durchlauf:
    0 - Start
    28 - channel read
    3703 - zeros calc.
    3950 - loop 1

    Somit dauern zurzeit die Filterund und die Berechnung der Nullstellen relativ am längsten.
    Allerdings wird die innere For-Schleife ca. 3000 mal durchlaufen. Ob man diese parallelisieren könnte, weiß ich noch nicht genau. Wohl nicht.
    Aber über die äußere For Schleife sollte es gehen... Immerhin 3 bzw. 6 durchläufe.
    die Stopzeiten sind ungeschickt verteilt. Führe mal dieses aus:

    VB.NET-Quellcode

    1. Public Sub CalculateHarmonics()
    2. If Not IO.File.Exists(SourceFile) Then
    3. RaiseEvent ErrorOccur(True, SourceFile & " not found")
    4. Exit Sub
    5. End If
    6. Dim sw = Stopwatch.StartNew
    7. Dim MyFamos As New ImcFamosLib.Famos
    8. MyFamos.ResultDataFormat = ImcFamosLib.FsResultFormatConstants.cfsFormatDouble
    9. MyFamos.Spec.FFTMode = ImcFamosLib.FsFFTModeConstants.cfsModeAppend
    10. MyFamos.Spec.FFTWindow = ImcFamosLib.FsFFTWindowConstants.cfsWinRectangle
    11. Dim famosFile As New ImcCoreLib.DFile
    12. Dim window As Integer = CInt(myProjectData.GridFrequency * 0.2)
    13. Console.WriteLine(channelNames.Length & " ChannelNames")
    14. For i = 0 To channelNames.Length - 1
    15. Dim raw As ImcCoreLib.DChannel
    16. famosFile.Open(_SourceFile, ImcCoreLib.DmFileOptionConstants.cdmFileQuickLoad)
    17. raw = GetFamosChannelFromFile(famosFile, channelNames(i), False)
    18. famosFile.Close()
    19. Console.WriteLine(sw.ElapsedMilliseconds & " - channel read")
    20. If IsNothing(raw) Then
    21. RaiseEvent ErrorOccur(False, "Channel " & channelNames(i) & " not found")
    22. Else
    23. raw.TransferToFamos()
    24. Dim samples As Integer = 8192
    25. If raw.xDelta = 0.00005 Then
    26. 'samples rate 20000
    27. samples = 4096
    28. End If
    29. 'Filter parameter: Butterworth,-,8th order, cut of freq. = 100 Hz
    30. Dim rawResample As ImcCoreLib.DChannel = MyFamos.Filter.LowPass(raw, 0, 0, 8, 100)
    31. rawResample.Name = raw.Name & "_lowpass"
    32. rawResample.TransferToFamos()
    33. 'Calculate the zero points; returns 'zero points' for 10 periods
    34. Dim rawZeros As ImcCoreLib.DChannel = CalculateZeroCrossings(raw, rawResample, window)
    35. rawZeros.Name = "Zeros"
    36. rawZeros.TransferToFamos()
    37. Console.WriteLine(sw.ElapsedMilliseconds & " - zeros calc.")
    38. Dim part As ImcCoreLib.DChannel
    39. Dim spec As ImcCoreLib.DChannel
    40. For j = 1 To 5 'rawZeros.Length - 1
    41. part = MyFamos.Edit.CopyPartX(raw, rawZeros.Value(j), rawZeros.Value(j + 1))
    42. part.Name = raw.Name & "_" & j
    43. part.TransferToFamos()
    44. 'part.xOffset = 0
    45. spec = CalculateFFT(MyFamos, part, (rawZeros.Value(j + 1) - rawZeros.Value(j)), samples)
    46. spec.Name = raw.Name & "_spec_" & j
    47. spec.TransferToFamos()
    48. 'TODO: work with spec(trum): calc/check min, max mean, RMS values
    49. Next
    50. Console.WriteLine(sw.ElapsedMilliseconds & " - FFTs calc.")
    51. End If
    52. Next
    53. End Sub
    Das vergleicht besser die ZeroCalcs mitte FftCalcs

    Tukuan schrieb:

    VB.NET-Quellcode

    1. If raw.xDelta = 0.00005 Then
    Wie genau wird der Wert 0.00005 in Double dargestellt?
    Gib hier ggf. einen Wertebereich vor (Toleranz).
    Mir scheint, dass das File-Handling alles andere überwiegt.
    Mit wievielen Punkten wird die FFT durchgeführt?
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Ich habe die Berechnung mit der geänderten Ausgabe mal durchgeführt und die Ergebnisse für die 6 durchläufe mal aufbereitet:

    in ms
    zeros calc.
    FFTs calc.
    min
    2237
    3269
    max
    5349
    21954
    mean
    3378,5
    10112,5

    Das Laden der der Kanäle dauert nur max. 72 ms - ofter sehr viel schneller.

    Jeder Datensatz ist 10 min lang und wir mit 40 kHz abgetastet. In die FFT gehen dann zurechgeschittene Kanäle mit einer länge von 10 Perioden bei 50 Hz - also ca. 200 ms. Somit haben sie ca. 8000 Datensätze.