Tight packing of storage variables is not unique to structs. From the Solidity documentation on the layout of state variables in storage:
Statically-sized variables (everything except mapping and dynamically-sized array types) are laid out contiguously in storage starting from position 0. Multiple, contiguous items that need less than 32 bytes are packed into a single storage slot if possible, according to the following rules:
The first item in a storage slot is stored lower-order aligned.
Elementary types use only that many bytes that are necessary to store them.
If an elementary type does not fit the remaining part of a storage slot, it is moved to the next storage slot.
Structs and array data always start a new slot and occupy whole slots (but items inside a struct or array are packed tightly according to these rules).
For non-storage variables, the use of
uint256 (and other 32 byte types) will be cheaper than non 32 byte counterparts as it avoids the need to convert prior to performing calculations:
When using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.
For properly laid out storage variables, the gas savings gained from tight packing more than justify the increased costs due to conversion.