Skip to content

Optimize vector creation/update using pg_VectorCoordsFromObj - #3458

Merged
oddbookworm merged 1 commit into
pygame-community:mainfrom
Starbuck5:small-vector-optimization
Jun 8, 2025
Merged

oddbookworm merged 1 commit into
pygame-community:mainfrom
Starbuck5:small-vector-optimization

Conversation

@Starbuck5

Copy link
Copy Markdown
Member

Rather than using pgVectorCompatible_Check and then PySequence_AsVectorCoords, which have duplicative operations, we can use pg_VectorCoordsFromObj to try to unpack directly, for a speedup when processing sequence input.

This also shrinks the code (nice for an optimization to do that).

I ran benchmarks and averaged results, here are the benchmarks that changed by 3% or more.

Code Speedup
Vector2(int tuple) 22%
Vector2(float tuple) 29%
Vector2(Vector2) 12%
Vector3(int, int, int) -9%
Vector3(float, float, float) -4%
Vector3(int tuple) 25%
Vector3(float tuple) 32%
Vector3(int broadcast) -7%
Vector3(float broadcast) -5%
Vector3(Vector3) 6%
Vector2.update(int tuple) 23%
Vector2.update(int broadcast) 4%
Vector2.update(Vector2) 13%
Vector3.update(int, int, int) 3%
Vector3.update(int tuple) 28%
Vector3.update(Vector3) 11%

I don't understand the slowdowns seen in Vector3 construction, because they don't happen in Vector2/3 update or Vector2 creation. If you read through the code of the 3 helper methods in the scope of this PR it should be unequivocally faster. But I didn't think it was worth just changing vector2, and I think the improvement is worth it overall. I'm interested if somebody not on MSVC gets the same results there.

Basic testing script
import pygame
import time


def test_create(cls, *args, iterations=1000000):
    start = time.time()
    for _ in range(iterations):
        x = cls(*args)
    return time.time() - start


print("Vector2()\t\t", test_create(pygame.Vector2))
print("Vector2(int, int)\t", test_create(pygame.Vector2, 24,12))
print("Vector2(float, float)\t", test_create(pygame.Vector2, 24.2,12.9))
print("Vector2(int tuple)\t", test_create(pygame.Vector2, (24,12)))
print("Vector2(float tuple)\t", test_create(pygame.Vector2, (24.2,12.9)))
print("Vector2(int)\t\t", test_create(pygame.Vector2, 24))
print("Vector2(float)\t\t", test_create(pygame.Vector2, 24.2))
print("Vector2(Vector2)\t", test_create(pygame.Vector2, pygame.Vector2(24.2,12.9)))
print("Vector2(str)\t\t", test_create(pygame.Vector2, repr(pygame.Vector2(24.2,12.9))))

print("Vector3()\t\t", test_create(pygame.Vector3))
print("Vector3(int, int)\t", test_create(pygame.Vector3, 24,12,4))
print("Vector3(float, float)\t", test_create(pygame.Vector3, 24.2,12.9,4.2))
print("Vector3(int tuple)\t", test_create(pygame.Vector3, (24,12,4)))
print("Vector3(float tuple)\t", test_create(pygame.Vector3, (24.2,12.9,4.2)))
print("Vector3(int)\t\t", test_create(pygame.Vector3, 24))
print("Vector3(float)\t\t", test_create(pygame.Vector3, 24.2))
print("Vector3(Vector3)\t", test_create(pygame.Vector3, pygame.Vector3(24.2,12.9,4.2)))
print("Vector3(str)\t\t", test_create(pygame.Vector3, repr(pygame.Vector3(24.2,12.9,4.2))))


def test_update(instance, *args, iterations=1000000):
    start = time.time()
    for _ in range(iterations):
        instance.update(*args)
    return time.time() - start


print("Vector2.update(int, int)\t", test_update(pygame.Vector2(24.2,12.9), 27.2, 23.1))
print("Vector2.update(int tuple)\t", test_update(pygame.Vector2(24.2,12.9), (27.2, 23.1)))
print("Vector2.update(int)\t\t", test_update(pygame.Vector2(24.2,12.9), 27.2))
print("Vector2.update(Vector2)\t\t", test_update(pygame.Vector2(24.2,12.9), pygame.Vector2(14.2,10.9)))
print("Vector3.update(int, int)\t", test_update(pygame.Vector3(24.2,12.9,4.2), 27.2, 23.1, 62.9))
print("Vector3.update(int tuple)\t", test_update(pygame.Vector3(24.2,12.9,4.2), (27.2, 23.1, 62.9)))
print("Vector3.update(int)\t\t", test_update(pygame.Vector3(24.2,12.9,4.2), 27.2))
print("Vector3.update(Vector3)\t\t", test_update(pygame.Vector3(24.2,12.9,4.2), pygame.Vector3(14.2,10.9,42.2)))

Rather than using pgVectorCompatible_Check and then PySequence_AsVectorCoords, which have duplicative operations, we can use pg_VectorCoordsFromObj to try to unpack directly for a speedup when processing sequence input.
@Starbuck5
Starbuck5 requested a review from a team as a code owner June 2, 2025 05:56
@Starbuck5 Starbuck5 added Performance Related to the speed or resource usage of the project math pygame.math labels Jun 2, 2025
@ankith26 ankith26 added this to the 2.5.6 milestone Jun 7, 2025

@ankith26 ankith26 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some benchmarking on Ubuntu 25.04 and found the results to be highly variable across different attempts. This one time I got all numbers in the 30% - 50% improvement range but in another attempt I got both branches to be roughly the same with this PR only marginally better.

I suppose this makes sense given the current time is low enough as it is, in such cases performance measurements no longer become super reliable. All we can say for sure is that it improves performance by some bit, and it makes the code simpler so it's good reason to approve this PR, thanks! πŸŽ‰

@ankith26

ankith26 commented Jun 7, 2025

Copy link
Copy Markdown
Member

I also noticed the pgVectorCompatible_Check -> PySequence_AsVectorCoords at other places in the file. It would be nice to take care of all of that as well, either in this PR or in a future one

@Starbuck5

Copy link
Copy Markdown
Member Author

We can say for sure that this should improve performance, it's not like I just moved stuff around, pg_VectorCoordsFromObj uses a subset of the calls that the other functions do combined.

Anyway there are some other instances but I would not like to do them in this PR, keep this PR simple and just do a few functions.

@oddbookworm

Copy link
Copy Markdown
Member

Hmmmm, I'm not so sure about this... I used timeit.repeat and functools.partial and here's my results that were fairly consistent across runs on windows 11, python 3.13

{'Vector2()': -7.28910036378114,
 'Vector2(Vector2)': -3.160696433735792,
 'Vector2(float)': -4.343867984836787,
 'Vector2(float-float)': -1.8094501908171339,
 'Vector2(float-tuple)': -21.70979851177708,
 'Vector2(int)': -0.8553595941494704,
 'Vector2(int-int)': -3.5897861152704493,
 'Vector2(int-tuple)': -14.383930090792063,
 'Vector2(str)': 0.14327725051727236,
 'Vector2.update(Vector2)': -6.017349623078288,
 'Vector2.update(int)': 8.008583955704868,
 'Vector2.update(int-int)': 0.14343824640653086,
 'Vector2.update(int-tuple)': -30.014621914195327,
 'Vector3()': 0.8899201813001486,
 'Vector3(Vector3)': -4.881886129006301,
 'Vector3(float)': -7.337099902911914,
 'Vector3(float-float)': 4.066558056509398,
 'Vector3(float-tuple)': -31.618105714994478,
 'Vector3(int)': -3.521999548794215,
 'Vector3(int-int)': 0.44965206624487497,
 'Vector3(int-tuple)': -20.536139978217793,
 'Vector3(str)': -1.5350113276052977,
 'Vector3.update(Vector3)': -4.824347217012836,
 'Vector3.update(int)': 0.6016414425170982,
 'Vector3.update(int-int)': 6.516772640752294,
 'Vector3.update(int-tuple)': -33.682215728994755}

(calculated as (pull_time - main_time) / main_time * 100)

@Starbuck5

Starbuck5 commented Jun 8, 2025 •

Copy link
Copy Markdown
Member Author

If pull time - main time is negative, then the pull request is running quicker, right? So I'm not sure why your results would make you unsure about this PR, most of your tests show a time reduction.

@oddbookworm

Copy link
Copy Markdown
Member
Here's my test script
import pygame
from timeit import repeat
from functools import partial


def test_create(cls, *args, iterations=1000000):
    return sum(repeat(partial(cls, *args), number = iterations))


print("Vector2()\t\t", test_create(pygame.Vector2))
print("Vector2(int-int)\t", test_create(pygame.Vector2, 24,12))
print("Vector2(float-float)\t", test_create(pygame.Vector2, 24.2,12.9))
print("Vector2(int-tuple)\t", test_create(pygame.Vector2, (24,12)))
print("Vector2(float-tuple)\t", test_create(pygame.Vector2, (24.2,12.9)))
print("Vector2(int)\t\t", test_create(pygame.Vector2, 24))
print("Vector2(float)\t\t", test_create(pygame.Vector2, 24.2))
print("Vector2(Vector2)\t", test_create(pygame.Vector2, pygame.Vector2(24.2,12.9)))
print("Vector2(str)\t\t", test_create(pygame.Vector2, repr(pygame.Vector2(24.2,12.9))))

print("Vector3()\t\t", test_create(pygame.Vector3))
print("Vector3(int-int)\t", test_create(pygame.Vector3, 24,12,4))
print("Vector3(float-float)\t", test_create(pygame.Vector3, 24.2,12.9,4.2))
print("Vector3(int-tuple)\t", test_create(pygame.Vector3, (24,12,4)))
print("Vector3(float-tuple)\t", test_create(pygame.Vector3, (24.2,12.9,4.2)))
print("Vector3(int)\t\t", test_create(pygame.Vector3, 24))
print("Vector3(float)\t\t", test_create(pygame.Vector3, 24.2))
print("Vector3(Vector3)\t", test_create(pygame.Vector3, pygame.Vector3(24.2,12.9,4.2)))
print("Vector3(str)\t\t", test_create(pygame.Vector3, repr(pygame.Vector3(24.2,12.9,4.2))))


def test_update(instance, *args, iterations=1000000):
    return sum(repeat(partial(instance.update, *args), number = iterations))


print("Vector2.update(int-int)\t", test_update(pygame.Vector2(24.2,12.9), 27.2, 23.1))
print("Vector2.update(int-tuple)\t", test_update(pygame.Vector2(24.2,12.9), (27.2, 23.1)))
print("Vector2.update(int)\t\t", test_update(pygame.Vector2(24.2,12.9), 27.2))
print("Vector2.update(Vector2)\t\t", test_update(pygame.Vector2(24.2,12.9), pygame.Vector2(14.2,10.9)))
print("Vector3.update(int-int)\t", test_update(pygame.Vector3(24.2,12.9,4.2), 27.2, 23.1, 62.9))
print("Vector3.update(int-tuple)\t", test_update(pygame.Vector3(24.2,12.9,4.2), (27.2, 23.1, 62.9)))
print("Vector3.update(int)\t\t", test_update(pygame.Vector3(24.2,12.9,4.2), 27.2))
print("Vector3.update(Vector3)\t\t", test_update(pygame.Vector3(24.2,12.9,4.2), pygame.Vector3(14.2,10.9,42.2)))
Here's a sample csv of my results, the first data column is your pull, the second data column is main
Vector2(),0.18141399999149144,0.1956771002151072
Vector2(int-int),0.3431495000841096,0.35592650016769767
Vector2(float-float),0.27297670010011643,0.278007100103423
Vector2(int-tuple),0.3280133999651298,0.3831212999066338
Vector2(float-tuple),0.25656020012684166,0.32770409993827343
Vector2(int),0.27341980033088475,0.2757786998990923
Vector2(float),0.2362144001526758,0.24694119987543672
Vector2(Vector2),0.2150278000626713,0.22204600006807595
Vector2(str),1.2432881002314389,1.2415092998417094
Vector3(),0.1823664001422003,0.18075780000071973
Vector3(int-int),0.43637820007279515,0.4344247999833897
Vector3(float-float),0.34569360001478344,0.33218510006554425
Vector3(int-tuple),0.35870049986988306,0.4514008000260219
Vector3(float-tuple),0.26794569997582585,0.39183720014989376
Vector3(int),0.27129990002140403,0.28120390011463314
Vector3(float),0.23677530023269355,0.25552330003120005
Vector3(Vector3),0.21837150014471263,0.22957930015400052
Vector3(str),1.6838350999169052,1.7100850999122486
Vector2.update(int-int),0.2269032000331208,0.22657820023596287
Vector2.update(int-tuple),0.19853689998853952,0.2836833999026567
Vector2.update(int),0.19830159994307905,0.18359799997415394
Vector2.update(Vector2),0.16061399993486702,0.17089750000741333
Vector3.update(int-int),0.2782052999828011,0.2611844999482855
Vector3.update(int-tuple),0.22197650000452995,0.33471639989875257
Vector3.update(int),0.18535450019408017,0.1842459999024868
Vector3.update(Vector3),0.1674094998743385,0.17589529987890273
Here's my csv analysis script
import csv

diffs: dict[str, float] = {}
with open("results.csv", "r") as data:
    csv_reader = csv.reader(data.readlines())

    for line in csv_reader:
        func = line[0]
        pull_value = float(line[1])
        main_value = float(line[2])

        percent_diff = ((pull_value - main_value) / main_value) * 100

        diffs[func] = percent_diff

from pprint import pprint

pprint(diffs)

@oddbookworm

Copy link
Copy Markdown
Member

If pull time - main time is negative, then the pull request is running quicker, right? So I'm not sure why your results would make you unsure about this PR, most of your tests show a time reduction.

You're right, I'm tired lol

@oddbookworm oddbookworm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, LGTM! πŸŽ‰

@oddbookworm
oddbookworm merged commit 584da82 into pygame-community:main Jun 8, 2025
@Starbuck5
Starbuck5 deleted the small-vector-optimization branch October 19, 2025 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

math pygame.math Performance Related to the speed or resource usage of the project

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants