The alternative is to store the data in a contiguous sequence of raw bytes. I am imagining some benefits to this approach, such as being able to read large extents of data without having to parse the block structure. But I can't quite put my finger on whether this is a real benefit or not.
In my experience and domain (Visual FX for films and games and so forth), it has been extremely beneficial, both in terms of performance and simplicity of the file savers and loaders, and without sacrificing extensibility, to favor large, contiguous, homogeneous blocks in file formats when possible as opposed to teeny little interleaved chunks. That said, our needs are much less sophisticated than like an RDMS. We just want to store all relevant data and read it all back efficiently in a rather sequential fashion, not model complex data structures like B+ trees on disk with lots of disk seeks to do binary searches and so forth. We do have strong extensibility requirements, however, since the computer graphics industry moves so quickly and constantly introduces game-changing concepts.
In particular we had this ancient file format which was binary and not particularly inefficient in terms of disk use or access (could be read sequentially, did not require seeking back and forth) but involved a lot of tiny chunks of variable-sized data interleaved with each other. And that sometimes made massive scenes artists created spanning gigabytes of data take 5 minutes to load, for example, not only in our native loaders but similar times when third party loaders would parse and load them.
Interleaving Tiny Variable-Sized Chunks
Specifically what was taking the most time as well as involving the most complex code was loading in geometry data (3D models), which would consist of vertex positions, UV texture coordinates, variable-length polygons (triangles, quads, n-gons with varying number of vertices), material data, etc.
The polygons in particular would be interleaved so loading them might involve parsing a quadrangle chunk and then checking to see how many vertices it has (4), then add 4 vertices to the mesh (or some auxiliary structure with the mesh operations deferred) and form a polygon from them, and the reader might then encounter a triangle (for which it'd have to do the same thing again) followed by a quadrangle followed by a triangle again followed by an n-gon and so forth.
Specifically interleaving all this tiny variable-sized data (a 3-vertex triangle interleaved with a 4-vertex quad, e.g.) made it so the loaders would have to stop and check the size of every little polygon chunk even to know how much data to read to move to the next chunk. And profiling the code didn't actually reveal hotspots in disk I/O. We were using efficient file I/O APIs that would buffer things and the disk access was straightforward and sequential. The hotspots were rather distributed across the board and coming more from all the extra branching and little virtual function calls and little memory allocations that sort of representation tended to promote.
Contiguous, Homogeneous, Fixed-Sized Chunks
So later on I was tasked to design a new file format, and actually not with efficiency as the primary reason for doing that (we wanted a simpler, fresher format that wasn't 2 decades old with all sorts of backwards compatibility support for legacy concepts that hadn't been in the software in over a decade). And one of the first things I did was avoid interleaving variable-sized data like polygons. Instead of:
triangle quad triangle n-gon
[3: v1v2v3][4: v1v2v3v4][3: v1v2v3][5: v1v2v3v4v5][....]
I did it like this:
[triangles ][count][v1v2v3][v1v2v3][v1v2v3][v1v2v3][v1v2v3][...]
[quadrangles][count][v1v2v3v4][v1v2v3v4][v1v2v3v4][v1v2v3v4][...]
And only for n-gons (polygons with 5 or more vertices, not frequently used by artists) would I interleave variable-length polygon chunks. And the main benefit I thought of doing that was that it really simplified our code because we could read all the triangles for a mesh not only in a single read call but also not have to get clever with how we allocated little chunks of memory efficiently since we knew exactly how much to allocate in advance to store all triangles (one huge memory allocation as opposed to a boatload of teeny ones). I didn't expect it to perform that much better on a first try since that legacy format had been optimized and tuned to death for decades by the team.
But, almost by accident, on my first try I ended up being able to load the same scene that took over 5 minutes to load in our legacy format in under 200 milliseconds. And I wasn't doing anything more as far as efficiency was concerned in the new format except just that (favoring bulkier, contiguous chunks of memory following a tag which is parsed over little teeny variable-sized chunks). So that really helped a lot on top of simplifying the entire format in our case.
Extensibility With Zip Files
Another thing that I don't think tied to efficiency but did simplify a lot is that our previous file format was sort of like how XML might look like if it was turned into binary (with nested "start/end tags" to indicate the beginning and end of blocks). I ended up just using the zip format here (uncompressed) and, in place of nested tags, I used folders and files stored in the zip file (ex: file.zip/scene/mesh.dat
instead of the binary equivalent of like <scene><mesh>...</mesh></scene>
). That also made it easier to inspect the file for how it was arranged using standard zip software as a secondary measure for analysis.
And that gave the desired extensibility since if we introduced some new concept (let's call it "Foo"), we can just add like a scene/Foo.dat
(or even a sub-directory/folder) to the zip file which newer versions of the loaders could pick up and read. We did give those files a proprietary extension to avoid confusion with general zip files, but it was basically just a zip file with stuff inside of it.
There might be one efficiency argument to favoring zip since our previous format favored sequential access for the most part (couldn't effectively skip data so well). It did identify how many bytes were in a given block/chunk in advance, so you could theoretically skip that stuff, but it was a bit unwieldy to do so and our native loaders as well as ones written by third parties tended to just favor reading everything for simplicity. With the ZIP format it's pretty easy to just ignore files and folders inside the ZIP that aren't of interest to whomever is loading the format (ex: a third party application), so skipping over data that isn't of interest becomes particularly easy there.