Seite 1 von 1
Aufteilen einer großen Klasse / trennung von Funktionalität.
Verfasst: Montag 24. August 2015, 11:29
von jens
Bei meiner MC6809 CPU Emulation, frage ich mich gerade, ob ich nicht die sehr große Klasse CPU() aufteilen sollte:
https://github.com/6809/MC6809/blob/mas ... cpu6809.py
Ich könnte natürlich das Aufteilen, durch erben machen. Dabei Frage ich mich allerdings, wie das mit der Performance aussieht.
Es gibt ja u.a. den Performance Tip, möglichst auf "dot-Names" zu verzichten und ehr auf lokale Variablen zurück zu greifen:
https://wiki.python.org/moin/PythonSpee ... iding_dots... (Aktuell down)
Wenn ich so einen Aufbau mache:
Code: Alles auswählen
class BaseCPU():
#...
class MC6809AddressModes(BaseCPU):
#...
class MC6809RegisterOperations(MC6809AddressModes):
#...
class MC6809LogicalOperations(MC6809RegisterOperations):
#...
class MC6809Branches(MC6809LogicalOperations):
#...
# usw
Das kommt mir hier schon ziemlich falsch vor. Und dann frage ich mich, wie das intern aufgelöst wird? Es wird doch dann die Hirachie immer weiter abwärts nach der Methode/Attribut gesucht, oder? Das dürfte dann ziemlich bremsen...
Andere Vorschläge wie man das Monster aufteilen kann?!?
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Montag 24. August 2015, 12:22
von BlackJack
@jens: Manche Sachen kann man halt nicht wirklich sinnvoll aufteilen. Muss man ja auch nicht unbedingt. Wenn überhaupt könntest Du die einzelnen Befehlsgruppen als Mixins lösen also:
Code: Alles auswählen
class RegisterOperations(object):
# ...
class LogicalOperations(object):
# ...
class CPU(RegisterOperations, LogicalOperations):
# ...
Dann könnte man auch einfach CPU-Variationen aus bestehenden Befehlssätzen plus den Abweichungen so zusammensetzen. Das `MC6809` habe ich aus den Namen mal rausgelassen, denn das Modul heisst ja schon wie die CPU.
Edit: Über die Performance würde ich mir erst Gedanken machen wenn es zum Problem wird.
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Montag 24. August 2015, 12:49
von jens
Stimmt, Mixins ist viel Sinnvoller!
Weil ursprünglich kommt die Idee davon:
z.Z. hab ich einen "Renne so schnell du kannst" Modus und ein "Nicht schneller als Echtzeit" Modus. Der letztere fügt quasi nur ein time.sleep() ein.
Also habe ich irgendwo im "mainloop" eine Art "if realtime:"... Das bremst natürlich, wenn Echtzeit nicht erwünscht ist... Oder wenn der Rechner auf dem es läuft einfach zu langsam dafür ist.
BlackJack hat geschrieben:Edit: Über die Performance würde ich mir erst Gedanken machen wenn es zum Problem wird.
Ist so eine Sache. Auf einem "Performanten" Rechner, bin ich ja schon mit CPython knapp über die Echtzeit.
z.B. Intel i7-4790K... (Wobei der ja im Singlecore-Performance fast führend ist)...
Mit PyPy ist es selbst auf meinem Uralt Laptop mit Single-Core 1,66GHz CPU fast 10x schneller als Echtzeit...
Auf der anderen Seite: Je besser der "mainloop" optimiert ist um so länger darf time.sleep() sein und um so weniger belastet ist die CPU und mehr Zeit ist für die GUI da...
Also Optimieren kann nicht schaden, in diesem Fall...
Wie setzt Python denn bei diesem Mixin-Ansatz den "Runtime-Code" zusammen?!? Landen alle Attribute/Methoden im __dict__ so das es im Prinzip keinen Performance Unterschied geben wird?
Ich denke ich werde das mal in einem Branch einfach mal testen...
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Montag 24. August 2015, 13:05
von BlackJack
@jens: ”Python” allgemein trifft da keine Aussagen. Das kommt also auf die Implementierung an. Keine Ahnung wie CPython das macht. Da wären wir aber IMHO schon wieder bei Mikrooptimierungen bei denen man eher die Sprache wechseln sollte statt jetzt Implementierungsdetails auszumessen.
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Montag 24. August 2015, 13:25
von jens
BlackJack hat geschrieben:@jens: ”Python” allgemein trifft da keine Aussagen. Das kommt also auf die Implementierung an. Keine Ahnung wie CPython das macht. Da wären wir aber IMHO schon wieder bei Mikrooptimierungen bei denen man eher die Sprache wechseln sollte statt jetzt Implementierungsdetails auszumessen.
Naja, ich werde bei Python bleiben. Auch wenn es nicht die optimale Sprache für eine Emulator ist
Auf der anderen Seite, greift der JIT von PyPy genau in diesem Szenario ja super...
Ich denke richtig "optimiert" könnte man eine CPU-Emulation per Code-Generator bauen. Aber das will ich wiederum nicht...
Also zu viel Mikrooptimierungen machen, dafür aber unschönen Code haben, will ich auch nicht. Halt so einen vernünftigen Zwischenweg...
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Montag 24. August 2015, 14:45
von jerch
jens hat geschrieben:Wie setzt Python denn bei diesem Mixin-Ansatz den "Runtime-Code" zusammen?!? Landen alle Attribute/Methoden im __dict__ so das es im Prinzip keinen Performance Unterschied geben wird?
Das ist ein MRO-Problem, dafür gibts ein paar Artikel, wie Python das umsetzt. Welche Laufzeitimplikationen das für verschiedene Python-Inkarnationen hat, kann ich Dir nicht sagen. Jenseits der Implementationsdetails sollte MRO-Verhalten allerdings bei allen gleich sein, da man keine Überraschungen bei Mehrfachvererbung zwischen CPython, Jython, PyPy etc. sehen möchte.
Generell zu Optimierungen:
- Kurzer Quelltext ist meist auch schneller, weil Quelltextlänge oft mit Anzahl der auszuwertenden Instruktionen korreliert (vorausgesetzt man produziert nicht seitenweise dead code oder Monsterbranches).
- Komplexe Typen vs. simple Typen - einfache Typen sind idR schneller. Das kommt aber mit einem tradeoff - idR ist die Funktionalität nicht 1:1 abbildbar und man braucht Zusatzcode (Bloatware), um die Funktionalität zu erhalten. Das geht dann schnell zu Lasten der Lesbarkeit bzw. kann der Zusatzcode sehr viel schlechter sein als der Librarycode, was dann eher alles schlimmer macht. Bei CPython kommt noch der Unterschied zwischen internen Typen (in C geschrieben) vs. selbst definierten hinzu. Eine clever gestrickte Liste mit Integern könnte evtl. das Gleiche leisten, wie eine selbst definierte Klasse usw. Ob man dagegen optimieren will - wohl eher nicht, da dass dann doch zu implementationsspezifisch sein dürfte.
- lokal vs. objektweit - Hmm, in C naher Programmierung macht das definitv was aus allein schon aufgrund der Heap/Stack-Unterschiede. Je nach Umsetzung/Durchreichung können in C/C++ geschriebene Interpreter auch in der interpretierten Sprache davon profitieren (z.B. zeigen alle JS-Interpreter/Jits dieses Verhalten). Zusätzlich brauchen objektweite Attribute einen weiteren Dereferenzierungsschritt beim Auflösen des Namens - mehr Arbeit für den Rechner. Ob Du hier etwas gewinnen kannst - schwer zu sagen, dafür bräuchtest Du ein genaues Profiling.
Generell würde ich mittels profiling schauen, ob es Flaschenhälse gibt, welche mit Mitteln der Sprache behebbar sind. Wenn dann die Performance noch nicht reichen sollte - Plan B für Funktionalität XY....
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Montag 24. August 2015, 16:55
von jens
So, ich hab mal, auf die schnelle, den mixins Ansatz gestartet:
https://github.com/6809/MC6809/compare/split_cpu
Erster Tests (mit dem eingebauten Benchmark, siehe:
https://github.com/6809/MC6809#readme ) zeigen das es kaum was ausmacht, von der Performance her...
Im Gegenteil: Es scheint sogar ein Tick schneller zu sein

:K
Werde noch weitere Funktionale Trennung vornehmen.
z.B. den "Speed Limit" (Für Echtzeit Lauffähigkeit) Code separieren...
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Montag 24. August 2015, 17:14
von jerch
@jens:
Wie viele Durchläufe/s schafft denn Dein Emulator-Loop in nicht optimiertem CPython?
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Montag 24. August 2015, 18:20
von jens
Ein Schleifendurchlauf macht ja kaum was. Deswegen sind es hunder tausende, aber bei pypy ein paar Millionen...
Wie gesagt, auf einem schnellen Rechner, kommt man auch mit cpython an Echtzeit heran.
Siehe auch:
https://github.com/jedie/DragonPy#performance
Allerdings stimmen die cpython Werte nicht mehr. Sind halt um die 800.000cycles/s... Jeder eigentlicher Schleifendurchlauf ist ein op code Abarbeitung die zwischen 2-4cycles sind...
Kommt also auch auf das Assembler Programm bzw. den Maschine code...
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Montag 24. August 2015, 21:06
von jens
So, ich hab nochmal mit mehreren Durchläufen vom benchmark mit CPython3 nachgemessen:
Die alte Variante (also eine mega Klasse):
450750 CPU cycles/sec
467793 CPU cycles/sec
448952 CPU cycles/sec
419205 CPU cycles/sec
457892 CPU cycles/sec
Also im Schnitt: 448918,4 CPU cycles/sec
Die mixins Variante (git commit c26c124a3816b9c391340bb6a72572fdd6926ddd ):
428119 CPU cycles/sec
460061 CPU cycles/sec
459328 CPU cycles/sec
422722 CPU cycles/sec
453091 CPU cycles/sec
Schnitt: 444664,2 CPU cycles/sec
Da die Schwankungen der einzelnen Durchläufe (die alle rund ~20Sek. dauern) so groß sind, fällt das wohl kaum ins Gewicht.
Zumal diese Modularität, ganz neue Perspektiven eröffnet

z.B. ist ein Trace-CPU wieder ohne weiteres Möglich. Außerdem bremst der "Speed-Limit" code nicht aus, wenn er gar nicht aktiv ist usw...
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Dienstag 25. August 2015, 08:14
von jens
Code: Alles auswählen
from __future__ import absolute_import, division, print_function
import sys
print(sys.version)
INTERNAL_LOOPS = 1000
class CPU(object):
def __init__(self):
self.memory = [0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02]
self.program_counter = 0x00
self.register_A = 0x00
def run(self):
for __ in range(INTERNAL_LOOPS):
self.program_counter = 0x00
self.register_A = 0x00
for __ in range(len(self.memory)):
self.next_opcode()
def next_opcode(self):
opcode = self.memory[self.program_counter]
self.program_counter += 1
if opcode == 0x01:
self.op_inc_a()
elif opcode == 0x02:
self.op_dec_a()
def op_inc_a(self):
self.register_A += 1
def op_dec_a(self):
self.register_A -= 1
#-----------------------------------------------------------------------------
class BaseCPU(object):
def __init__(self):
self.memory = [0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02]
self.program_counter = 0x00
self.register_A = 0x00
class RunMixin(object):
def run(self):
for __ in range(INTERNAL_LOOPS):
self.program_counter = 0x00
self.register_A = 0x00
for __ in range(len(self.memory)):
self.next_opcode()
def next_opcode(self):
opcode = self.memory[self.program_counter]
self.program_counter += 1
if opcode == 0x01:
self.op_inc_a()
elif opcode == 0x02:
self.op_dec_a()
class OpsMixin(object):
def op_inc_a(self):
self.register_A += 1
def op_dec_a(self):
self.register_A -= 1
class MixinCPU(BaseCPU, RunMixin, OpsMixin):
pass
#-----------------------------------------------------------------------------
def test_single_class():
single_class = CPU()
single_class.run()
assert single_class.register_A == -3
def test_mixin_class():
mixin_class = MixinCPU()
mixin_class.run()
assert mixin_class.register_A == -3
if __name__ == "__main__":
from timeit import Timer
test_single_class()
test_mixin_class()
def timeit(func, number):
name = func.__name__
sys.stdout.write("%20s: " % name)
sys.stdout.flush()
t = Timer("%s()" % name, setup="from __main__ import %s" % name)
print("%.3fsec" % t.timeit(number))
number = 1000
timeit(test_single_class, number)
timeit(test_mixin_class, number)
Ausgabe:
3.4.3 (v3.4.3:9b73f1c3e601, Feb 24 2015, 22:43:06) [MSC v.1600 32 bit (Intel)]
test_single_class: 4.319sec
test_mixin_class: 4.282sec
Ist wirklich ein Tick schneller?!? Warum?
EDIT: und mit Py2:
Code: Alles auswählen
2.7.10 (default, May 23 2015, 09:40:32) [MSC v.1500 32 bit (Intel)]
test_single_class: 3.656sec
test_mixin_class: 3.641sec
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Dienstag 25. August 2015, 10:12
von jens
Nochmal...
Hab die Klasse noch weiter aufgeteilt (Auch wenn da so keinen Sinn macht) und noch cProfile() lauf dazu gepackt:
Code: Alles auswählen
from __future__ import absolute_import, division, print_function
from timeit import Timer
import cProfile
import sys
print(sys.version)
INTERNAL_LOOPS = 20000
class CPU(object):
def __init__(self):
self.memory = [0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02]
self.program_counter = 0x00
self.register_A = 0x00
def run(self):
for __ in range(INTERNAL_LOOPS):
self.program_counter = 0x00
self.register_A = 0x00
for __ in range(len(self.memory)):
self.next_opcode()
def next_opcode(self):
opcode = self.memory[self.program_counter]
self.program_counter += 1
if opcode == 0x01:
self.op_inc_a()
elif opcode == 0x02:
self.op_dec_a()
def op_inc_a(self):
self.register_A += 1
def op_dec_a(self):
self.register_A -= 1
#-----------------------------------------------------------------------------
class BaseCPU(object):
def __init__(self):
self.memory = [0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02]
self.program_counter = 0x00
self.register_A = 0x00
class Run1Mixin(object):
def run(self):
for __ in range(INTERNAL_LOOPS):
self.program_counter = 0x00
self.register_A = 0x00
for __ in range(len(self.memory)):
self.next_opcode()
class Run2Mixin(object):
def next_opcode(self):
opcode = self.memory[self.program_counter]
self.program_counter += 1
if opcode == 0x01:
self.op_inc_a()
elif opcode == 0x02:
self.op_dec_a()
class Ops1Mixin(object):
def op_inc_a(self):
self.register_A += 1
class Ops2Mixin(object):
def op_dec_a(self):
self.register_A -= 1
class MixinCPU(BaseCPU, Run1Mixin, Run2Mixin, Ops1Mixin, Ops2Mixin):
pass
#-----------------------------------------------------------------------------
def test_single_class():
single_class = CPU()
single_class.run()
assert single_class.register_A == -3
def test_mixin_class():
mixin_class = MixinCPU()
mixin_class.run()
assert mixin_class.register_A == -3
test_single_class()
test_mixin_class()
if __name__ == "__main__":
def timeit(func, number):
name = func.__name__
sys.stdout.write("%20s: " % name)
sys.stdout.flush()
t = Timer("%s()" % name, setup="from __main__ import %s" % name)
print("%.3fsec" % t.timeit(number))
def profile_it(func, number):
name = func.__name__
sys.stdout.write("%20s:" % name)
sys.stdout.flush()
cProfile.run(
'for __ in range(%(number)i):%(name)s()' % {
"name":name,
"number": number,
}
)
number = 100
timeit(test_single_class, number)
timeit(test_mixin_class, number)
profile_it(test_single_class, number)
profile_it(test_mixin_class, number)
Py2:
Code: Alles auswählen
2.7.10 (default, May 23 2015, 09:40:32) [MSC v.1500 32 bit (Intel)]
test_single_class: 7.282sec
test_mixin_class: 7.290sec
test_single_class: 32000403 function calls in 10.249 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 10.249 10.249 <string>:1(<module>)
100 0.000 0.000 0.000 0.000 test.py:17(__init__)
100 3.007 0.030 10.249 0.102 test.py:22(run)
14000000 5.054 0.000 6.894 0.000 test.py:29(next_opcode)
4000000 0.499 0.000 0.499 0.000 test.py:38(op_inc_a)
10000000 1.341 0.000 1.341 0.000 test.py:41(op_dec_a)
100 0.000 0.000 10.249 0.102 test.py:84(test_single_class)
2000000 0.073 0.000 0.073 0.000 {len}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
2000101 0.275 0.000 0.275 0.000 {range}
test_mixin_class: 32000403 function calls in 10.474 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 10.474 10.474 <string>:1(<module>)
100 0.000 0.000 0.000 0.000 test.py:47(__init__)
100 3.085 0.031 10.474 0.105 test.py:53(run)
14000000 5.127 0.000 7.026 0.000 test.py:61(next_opcode)
4000000 0.530 0.000 0.530 0.000 test.py:71(op_inc_a)
10000000 1.369 0.000 1.369 0.000 test.py:75(op_dec_a)
100 0.000 0.000 10.474 0.105 test.py:89(test_mixin_class)
2000000 0.076 0.000 0.076 0.000 {len}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
2000101 0.287 0.000 0.287 0.000 {range}
Py3:
Code: Alles auswählen
3.4.3 (v3.4.3:9b73f1c3e601, Feb 24 2015, 22:43:06) [MSC v.1600 32 bit (Intel)]
test_single_class: 8.346sec
test_mixin_class: 8.329sec
test_single_class: 30000303 function calls in 11.274 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 11.274 11.274 <string>:1(<module>)
100 0.000 0.000 0.000 0.000 test.py:17(__init__)
100 3.290 0.033 11.273 0.113 test.py:22(run)
14000000 5.873 0.000 7.909 0.000 test.py:29(next_opcode)
4000000 0.623 0.000 0.623 0.000 test.py:38(op_inc_a)
10000000 1.413 0.000 1.413 0.000 test.py:41(op_dec_a)
100 0.000 0.000 11.274 0.113 test.py:84(test_single_class)
1 0.000 0.000 11.274 11.274 {built-in method exec}
2000000 0.074 0.000 0.074 0.000 {built-in method len}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
test_mixin_class: 30000303 function calls in 11.183 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 11.183 11.183 <string>:1(<module>)
100 0.000 0.000 0.000 0.000 test.py:47(__init__)
100 3.272 0.033 11.182 0.112 test.py:53(run)
14000000 5.810 0.000 7.836 0.000 test.py:61(next_opcode)
4000000 0.624 0.000 0.624 0.000 test.py:71(op_inc_a)
10000000 1.402 0.000 1.402 0.000 test.py:75(op_dec_a)
100 0.000 0.000 11.183 0.112 test.py:89(test_mixin_class)
1 0.000 0.000 11.183 11.183 {built-in method exec}
2000000 0.074 0.000 0.074 0.000 {built-in method len}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
Fazit: Der Unterschied zwischen mixin und nicht-mixin ist inrelevant, gerade auch im Vergleich zum Unterschied Py2 vs. Py3.
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Dienstag 25. August 2015, 12:18
von jerch
@jens:
Unter Python2.7 wirken folgende Änderungen Wunder:
Code: Alles auswählen
class CPU(object):
def __init__(self):
self.memory = [0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02]
self.program_counter = 0x00
self.register_A = 0x00
def run(self):
local_registers = [self.program_counter, self.register_A]
memory = self.memory
next = self.next_opcode
for _ in xrange(INTERNAL_LOOPS):
local_registers[0] = 0
local_registers[1] = 0
for _ in xrange(len(memory)):
next(local_registers, memory)
self.program_counter, self.register_A = local_registers
def next_opcode(self, registers, memory):
opcode = memory[registers[0]]
registers[0] += 1
if opcode == 0x01:
registers[1] += 1
elif opcode == 0x02:
registers[1] -= 1
Idee ist, innerhalb der Schleifen unnötige Lookups zu entfernen, heisst Attribut- oder Methodenlookup (self.) im lokalen Namensraum zu halten. Achso und natürlich range vs. xrange...
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Dienstag 25. August 2015, 12:28
von jens
jerch hat geschrieben:Idee ist, innerhalb der Schleifen unnötige Lookups zu entfernen, heisst Attribut- oder Methodenlookup (self.) im lokalen Namensraum zu halten.
Mache ich teilweise. z.B. hier:
https://github.com/6809/MC6809/blob/028 ... #L286-L287
Natürlich kann man das extrem ausbauen. Aber das führt zu sehr unschönem code. Das will ich dann auch nicht
jerch hat geschrieben:Achso und natürlich range vs. xrange...
Im eigentlich code nutzte ich auch xrange in py2 und range in py3...
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Dienstag 25. August 2015, 12:40
von jerch
@jens:
Alles innerhalb Deiner self.get_and_call_next_op Methode ist "busy loop", d.h. jeder kleine Zusatzaufwand summiert sich auf. Und von da absteigend hast Du immernoch self.-Zugriffe drin, diese sind halt teurer als lokale. Was spricht dagegen, den äusseren Zustand per Parameter an den lokalen Namensraum mitzugeben (siehe memory-Übergabe im Bsp. oben)?
Bei Deiner Registerimplementation nutzt Du u.a. getter und setter-Methoden. Die sind deutlich teurer in CPython als der direkte lesende/schreibende Attributzugriff. Da die Registermanipulation die Hauptarbeit der CPU ist, wirkt sich das massiv aus.
Ich vermute, dass Du mit Umstellung auf lokale Namen und direkten Attributzugriff die Cyclerate der CPython-Version etwa verdoppelt könntest.
Re: Aufteilen einer großen Klasse / trennung von Funktionali
Verfasst: Dienstag 25. August 2015, 12:56
von jens
jerch hat geschrieben:Alles innerhalb Deiner self.get_and_call_next_op Methode ist "busy loop"...
Zu den generellen Optimierungen, hab ich dir bei
http://www.python-forum.de/viewtopic.ph ... 62#p282162 geantwortet. IMHO passt das dort besser.
Hier geht es um Mega-class-Aufteilen. Allerdings werde ich wohl eh bei der Aufteilung per Mixin bleiben, denke ich mir.