My code run very very slow. How can I make my python code that runs faster?

215 Views Asked by At

please help me.

I wrote a program to run "Game of Life" with PyQt6 but, it runs very very slow. How can I make it faster?

  1. What about using multiple cores of the CPU? How can a Python program run on multiple cores?
  2. What about doing display stuff with GPU? How can a Python program work with the GPU?
  3. What about writing the code itself faster? I know Python is not a fast language but, what can I do to improve the speed of code as much as possible?

main.py

from numpy import empty
from sys import argv
from cell import cell
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QBrush, QColor, QKeyEvent, QPainter, QPen, QPaintEvent
from PyQt6.QtWidgets import QApplication, QWidget

class Window(QWidget):
    def __init__(self, parent = None) -> None:
        super().__init__(parent)
        self.windowWidth = 1000
        self.windowHeight = 1000
        self.resolution = 2
        self.cells = empty((self.windowWidth // self.resolution, self.windowHeight // self.resolution), dtype=object)
        cellsShape = self.cells.shape
        for i in range(cellsShape[0]):
            for j in range(cellsShape[1]):
                self.cells[i][j] = cell(i, j, self.resolution)
        self.setWindowTitle("Game of Life")
        self.setGeometry((1920 - 800) // 2, (1080 - 800) // 2, 800, 800)
        self.setStyleSheet("background-color:rgb(20, 20, 20);")
        self.graphicTimer = QTimer(self)
        self.baseTimer = QTimer(self)
        self.graphicTimer.timeout.connect(self.update)
        self.graphicTimer.start(30)
        self.show()

    def run(self) -> None:
        for rows in self.cells:
            for cell in rows:
                cell.calculateNewState(self.cells)
        for rows in self.cells:
            for cell in rows:
                cell.setNewState()

    def keyPressEvent(self, event: QKeyEvent) -> None:
        key = event.key()
        if key == Qt.Key.Key_S:
            self.closeWindow()
        event.accept()

    def paintEvent(self, event: QPaintEvent) -> None:
        self.run()
        painter = QPainter()
        painter.begin(self)
        painter.setPen(QPen(QColor(20, 20, 20),  -1, Qt.PenStyle.SolidLine))
        painter.setBrush(QBrush(QColor(255, 255, 255), Qt.BrushStyle.SolidPattern))
        for rows in self.cells:
            for cell in rows:
                if cell.state == True:
                    painter.drawRect(cell.posX * cell.cellWidth, cell.posY * cell.cellWidth, cell.cellWidth, cell.cellWidth)
        painter.end()
        event.accept()

    def closeWindow(self) -> None:
        print("closing window ...")
        self.close()


if __name__ == "__main__":
    App = QApplication(argv)
    window = Window()
    exit(App.exec())

cell.py

from numpy.random import choice
from PyQt6.QtWidgets import QWidget


class cell(QWidget):
    def __init__(self, x, y, width, state = None, parent = None) -> None:
        super().__init__(parent)
        self.posX = x
        self.posY = y
        self.cellWidth = width
        self.state = state
        if self.state == None:
            self.state = choice([True, False])
        self.newState = None
        self.aliveNeighbors = 0
    
    def countAliveNeighbors(self, cells) -> None:
        cellsShape = cells.shape
        self.aliveNeighbors = 0
        for i in range(-1, 2):
            for j in range(-1, 2):
                neighbor = cells[(self.posX + i + cellsShape[0]) % cellsShape[0]][(self.posY + j + cellsShape[1]) % cellsShape[1]]
                if neighbor.state == True:
                    self.aliveNeighbors += 1
        if self.state == True:
            self.aliveNeighbors -= 1

    def calculateNewState(self, cells) -> None:
        self.countAliveNeighbors(cells)
        if self.state == False and self.aliveNeighbors == 3:
            self.newState = True
        elif self.state == True and (self.aliveNeighbors > 3 or self.aliveNeighbors < 2):
            self.newState = False
        else:
            self.newState = self.state
    
    def setNewState(self) -> None:
        self.state = self.newState
        self.newState = None

I want to write code that runs as fast as possible

3

There are 3 best solutions below

3
Tim On

You could try importing and using threading so that each task that doesn't necessarily need to be completed in order can be done simultaneously and speed things up. It could look something like this

 import threading
    x = threading.Thread(target=The_Target_Function, args=("YOUR ARGUMENTS",))
    x.start()
7
smcjones On

The simplest fix you can set for yourself is to pre-calculate your cell neighbors rather than running the calculation within an O(n^2) complexity. The modulo operation can be one of the slower operations to perform, so minimizing the number of times you run it will be good.

Ultimately you want to minimize or eliminate regular O(n^2) calculations to minimize CPU strain. Given that you’re not doing anything super complicated here, I think this is a simple and quick performance gain you can realize.

Remember that Python passes values by reference, so just initialize your neighbors in __init__ rather than passing them to calculateNewState.

If you’re running into performance issues, a good helper is dis.dis to see the steps in assembly code. Find your bottlenecks as each step is something your CPU needs to manage. Then, if you can remove it, your code will have fewer steps and run faster. Sometimes without seeing the low level code, it can be hard to understand where your bottlenecks really are.

4
AirSquid On

You can do a lot better by changing the structure of the core to use a matrix of "alive" cells represented by a numpy array of 0/1. If you do this, you can chop out the double loop you have and let numpy do the heavy lifting of neighbor counting. You could do some work to index that and get summations, but the 2nd enhancement is to use the process of "convolution with a kernel" which is very common in image processing. Basically you are taking a kernel (in this case a 3x3 kernel to hit the neighbors) and multiplying that by the appropriate grid in your matrix for every member. scipy has an image processing package that makes this a snap. Note that you can set up the kernel to ignore out-of-bounds by filling it with zero (zero padding is the term.)

I modified your neighbor counting function a bit to separate it from the cell class for comparison, but didn't change any of the guts.

In a 500 x 500 structure, your approach takes about 1.3 seconds to calculate the number of living neighbors. So that looping costs roughly 1 second per frame to do. Using convolution, I can get it done in 0.004 seconds, around a 1000x speed-up.

Note that when I compared results I got a failed comparison because I don't think your modulo arithmetic is accurate for the edges (it is wrapping--not desired.) You can see it with my code if you print the first neighbor matrix.

After you "neighbor count" you can use a couple numpy functions to figure out what the alive matrix looks like for the next evolution and just incorporate that in your code.

Code:

# game of life core

import numpy as np
from scipy import ndimage
from time import time

dimensions = 500, 500  # 5, 5  # small test

alive_matrix = np.random.randint(0,2,dimensions[0]*dimensions[1]).reshape(dimensions)
neighbor_kernel = [[1, 1, 1],
                   [1, 0, 1],
                   [1, 1, 1]]

print('starting alive matrix:')
print(alive_matrix)

def countAliveNeighbors(loc, cells) -> None:
    cellsShape = cells.shape
    aliveNeighbors = 0
    # print(loc)
    for i in range(-1, 2):
        for j in range(-1, 2):
            x = (loc[0] + i + cellsShape[0]) % cellsShape[0]
            y = (loc[1] + j + cellsShape[1]) % cellsShape[1]
            # print(x,y)
            neighbor = cells[x][y]
            if neighbor == 1:
                aliveNeighbors += 1

    if cells[loc[0]][loc[1]] == 1:
        aliveNeighbors -= 1
    return aliveNeighbors

# using looping
tic = time()
living_neighbors = np.empty(dimensions, dtype=int)
for row in range(alive_matrix.shape[0]):
    for col in range(alive_matrix.shape[1]):
        living_neighbors[row][col] = countAliveNeighbors((row, col), alive_matrix)
toc = time()
print(f'using loop: {toc-tic:0.3} sec')

# using convolution with kernel
tic = time()
living_neighbors_2 = ndimage.convolve(alive_matrix, neighbor_kernel, mode='constant', cval=0)
toc = time()
print(f'using convolution: {toc-tic:0.3} sec')

# assert np.array_equal(living_neighbors, living_neighbors_2)  # <- FAILS.  I think your modulu arithmetic is bad


print('living neighbor count:')
print(living_neighbors_2)

# simplify next alive...
stays_alive = alive_matrix & np.where((living_neighbors_2 >= 2) & (living_neighbors_2 <= 3), 1, 0)
born = np.where(alive_matrix==0, 1, 0) & np.where(living_neighbors_2 == 3, 1, 0)

next_alive = stays_alive | born

print('next alive matrix:')
print(next_alive)

Output:

starting alive matrix:
[[1 1 0 ... 1 0 1]
 [1 0 1 ... 1 0 0]
 [0 0 0 ... 0 1 0]
 ...
 [0 1 0 ... 0 0 1]
 [0 1 1 ... 1 1 1]
 [0 1 0 ... 0 0 0]]
using loop: 1.27 sec
using convolution: 0.00409 sec
living neighbor count:
[[2 3 2 ... 2 3 0]
 [2 4 1 ... 4 4 2]
 [2 3 2 ... 6 2 2]
 ...
 [3 3 5 ... 6 7 4]
 [3 3 4 ... 3 3 2]
 [2 2 4 ... 3 3 2]]
next alive matrix:
[[1 1 0 ... 1 1 0]
 [1 0 0 ... 0 0 0]
 [0 1 0 ... 0 1 0]
 ...
 [1 1 0 ... 0 0 0]
 [1 1 0 ... 1 1 1]
 [0 1 0 ... 1 1 0]]