Skip to content

Move i2c_struct.UnaryStruct's byte array to init - #62

Merged
tannewt merged 1 commit into
adafruit:mainfrom
grgrant:move-bytearray-to-init
Aug 18, 2026
Merged

Move i2c_struct.UnaryStruct's byte array to init#62
tannewt merged 1 commit into
adafruit:mainfrom
grgrant:move-bytearray-to-init

Conversation

@grgrant

@grgrant grgrant commented Aug 4, 2026

Copy link
Copy Markdown

After discussing with @tannewt he agreed this change was a reasonable one to make.

This brings i2c_struct.UnaryStruct's bytearray allocation into the class init so it matches all the other classes in the adafruit_register group. Because the UnaryStruct is used by things like tmp117._raw_temperature it was creating and releasing bytestrings each call. Now one is permanently allocated -- again like all the other classes here.

My testing shows that for the tmp117 library this new way of pre-allocating the bytearray for each of the UnaryStruct register accessors increases the per instance bytecount from 42 bytes to 74 bytes so a net increase per accessor of 32 extra bytes. However we avoid garbage collection churn.

I have been running this new library on 2 TMP119s and 1 TMP117 and 4 MAX1704x for a number of hours with no issues. I also read, wrote and read back the changed value of device registers on the TMP119 and MAX1704x devices.

All that being said, I hope given the very wide use of the adafruit_register libraries that whomever reviews is satisfied that my change is a reasonable and sane one.

@tannewt tannewt 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.

Could we allocate it once on first use? It'll cost us a little code size but save instance size for unused accessors.

@grgrant

grgrant commented Aug 4, 2026

Copy link
Copy Markdown
Author

@tannewt Are we worried at all about concurrency / two instances writing on the shared buffer?

Edit: I have implemented a class wide version of all the i2c_ libraries (except at the moment i2c_struct_array). See subsequent comments.

Here is the code change for UnaryStruct I'm currently testing. Similar for the others.

class UnaryStruct:
    _scratch = bytearray(1)          # ONE buffer shared by every UnaryStruct instance

    def __init__(self, register_address, struct_format):
        self.format = struct_format
        self.address = register_address
        self.size = struct.calcsize(struct_format)
        need = 1 + self.size
        if len(UnaryStruct._scratch) < need:      # grow at class-def time to fit the biggest
            UnaryStruct._scratch = bytearray(need)

    def __get__(self, obj, objtype=None):
        buf = UnaryStruct._scratch
        buf[0] = self.address                     # MUST set every call now (shared, gets clobbered)
        with obj.i2c_device as i2c:
            i2c.write_then_readinto(buf, buf, out_end=1, in_start=1, in_end=1 + self.size)
        return struct.unpack_from(self.format, buf, 1)[0]

    def __set__(self, obj, value):
        buf = UnaryStruct._scratch
        buf[0] = self.address
        struct.pack_into(self.format, buf, 1, value)
        with obj.i2c_device as i2c:
            i2c.write(buf, end=1 + self.size)

@grgrant

grgrant commented Aug 4, 2026

Copy link
Copy Markdown
Author

@tannewt Here are the footprint changes measured via gc.collect() + gc.mem_free() paired before and after. Sadly this particular optimization doesn't help TMP117 a ton because there are only 9 Bit(s) accessors and 5 UnaryStruct accessors -- on the order of 300 total bytes saved.

The CV attribute naming infrastructure in TMP117 takes 1280 bytes which dwarfs in savings.

Per instance space footprint

Class Orig Instance Class Buffer Saving
UnaryStruct 42 74 58 22%
RO/WBits 116 84 28%
RO/WBit 90 58 36%

Given the above, do you think I should fold in the class wide buffer version of these?

I haven't yet considered the register_ versions that I know @FoamyGuy has most recently worked on. If a similar savings were available there it could be about a 1K savings for the BMP5xx driver's 29 accessors.

@tannewt

tannewt commented Aug 5, 2026

Copy link
Copy Markdown
Member

@tannewt Are we worried at all about concurrency / two instances writing on the shared buffer?

Not currently. We don't support concurrency from Python. Even with asyncio property reads are sync.

I hadn't thought about a class-wide buffer and now I'm wondering if a module-wide one would be even better. What do you think?

@grgrant

grgrant commented Aug 5, 2026 via email

Copy link
Copy Markdown
Author

@grgrant

grgrant commented Aug 5, 2026

Copy link
Copy Markdown
Author

I hadn't thought about a class-wide buffer and now I'm wondering if a module-wide one would be even better. What do you think?

@tannewt I've done some research and i don't think there is any big benefit to a module wide one for most of i2c_ modules. I've verified that ROBit & Bits actually use the parent class's storage when they inherit RWBit & Bits definition. So I don't think there is any win at all there. The Struct and Struct array might benefit slightly but I'm not sure it is worth it. Already ROUnary inherits Unary and all Classes start out at one byte across all instances until instantiated and grown dynamically.

I'm also not sure that allocating on first use wins us much. If there are a dozen accessors in a library, what is the chance that none of them are ever used and the first use of ROBits for example amortizes all RO/WBits instances at once.

I tested new versions of adafruit_register.register_Bit(s) and Struct at the class level and here are the savings:

Class Instance Class Buffer Saving
UnaryStruct 80 48 40%
RO/WBits 154 112 27%
RO/WBit 96 64 33%

The memory footprint savings for adafruit_bmp5xx (29 accessors) with this change is:

3728 - 2800 = 928 so almost 1K

I'd really like to know if I should push these class-wide variables changes up or not. My original commit was just to bring the i2c_struct.UnaryStruct into line with all the others using an instance level buffer rather than allocating it on each get/set -- I could go either way on the class wide one. I'm ready to push all the libraries and have tested all but i2c_struct.Struct and i2c_struct_array because I don't have hardware using those.

HW using i2c_struct_array:

PCA9685
MPU6050
LSM303
LPS28
EMC2101
DS1841
AS7343

Or if you want me to change all of them to module wide, I think I know how to do the change..

@grgrant
grgrant requested a review from tannewt August 6, 2026 02:16
@tannewt

tannewt commented Aug 6, 2026

Copy link
Copy Markdown
Member

Or if you want me to change all of them to module wide, I think I know how to do the change..

Yup, give module wide a try. That will leave us with one buffer to share.

Note that CP/MP have a minimum allocation size so a one byte allocation is really 16 bytes x 2. One for the buffer and one for the wrapper object.

@grgrant

grgrant commented Aug 6, 2026

Copy link
Copy Markdown
Author

Yup, give module wide a try. That will leave us with one buffer to share.

Two ways to go with the module wide.

New class to store the module wide shared variable: e.g. new class named _scratch with shared buffer named buf. Called out in each other class as '_scratch.buf'

Global variable called eg as 'buf' but needs a noqa PLW0603 -- simpler to write throughout if you're ok with disabling the RUFF complaint.

@tannewt

tannewt commented Aug 7, 2026

Copy link
Copy Markdown
Member

Global variable called eg as 'buf' but needs a noqa PLW0603 -- simpler to write throughout if you're ok with disabling the RUFF complaint.

I think if you capitalize it right it won't complain. I'd do this. The new class idea is unusual.

@grgrant

grgrant commented Aug 12, 2026

Copy link
Copy Markdown
Author

Note this could be superseded by #63

I think if #63 is rejected as too-big-of-a change then this should be done at minimum to reduce GC churn by making UnaryStruct match all the other classes by allocating once in init.

@tannewt
tannewt merged commit bb0256b into adafruit:main Aug 18, 2026
1 check passed
@grgrant
grgrant deleted the move-bytearray-to-init branch August 19, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants