Saving memory in an array/class members without losing too much performance

Viewed 36

I have a programms which stores 100.000s of files and its information. Typically the class definition is like this:

class DSPhotoFile: NSObject, NSCopying, Comparable {
    let key: Int                    // may I need an numeric ID? Easy and fast
    let initUrl              : URL  // initial, never changed (could also be used as key)
    // derived data from filepath
    // string parts
    var fullpath        = String()   // "/rootfolder/sub/sub2/pic1234.jpg"
    var path            = String()   // "/rootfolder/sub/sub2"  (path without tailing /)
    ...                              //  12345678901234567890 12345678901 
}

I want to save memory, but not at all costs. First: How can I dynamically calculate the amount of bytes an instance of this definition uses? There are string members which are dynamic! So I need the complete occupied space including all subtypes.

My next idea is to store only an Int for the length of the suffix or prefix if needed. E.g. path is the first 20 items of fullpath. If I store the Int16 "20", it needs 2 bytes, and then I calculate the member path as fullpath.prefix(20). This could also be done for all other members which rely on another constant string. Any idea about performance difference from String vs String.prefix? Some recommendations?

1 Answers

There is no easy way to compute the actual memory requirements of one record. As you note, the data is stored in a number of complicated ways that frustrate that. What is better is to run Instruments on an app that generates 100k real records and look at the total memory usage.

Making this an NSObject subclass is likely going to be wasteful compared to a struct (or even a pure Swift class, but I'd still start with a struct).

If your paths may be long, you can likely save substantial memory by storing the directory information separately and just storing the name. Copies of a string enjoy copy-on-write benefits, but strings that happen to be identical (but from different sources) do not get de-duplicated. So if you have something like:

let directory = "..."
let file1 = File(directory: directory, name: "file1")
let file2 = File(directory: directory, name: "file2")

Then you should expect the two copies of directory to share storage. This could matter a lot if the directory can be long. However:

let file1Name = "longdirectorypath/file1"
let file2Name = "longdirectorypath/file2"
let file1dir = String(file1Name.prefix(dirlen))
let file2dir = String(file2Name.prefix(dirlen))

In this case, you should not expect file1dir and file2dir to share storage, even though the string happens to have the same value.

Alternately, if you store you data in a tree structure (likely using Swift classes, not structs), then you may be able to avoid ever storing the whole path. Just store your parent and compute the path by walking the tree.

String.prefix returns a Substring, which is an O(1) operation, and very fast to access. So that's a fine approach, but I think still more expensive if you have many copies of the same directory path.

I would explore the cost of URL vs String for initUrl. I expect that URL is a bit larger (and you can compute an URL if you need it).

If you're just going to have a ton of URLs in kind of random order, so it's hard to share parts of the string, then I'd probably just store initUrl as a String, and slice it up for fullpath and path if possible. As long as they all live together in a struct, you can store a Substring (which is just a range), or use use two integers to point to the path start and the filename start. With those you can compute the rest.

If you're precomputing things for performance, make sure to profile that carefully to make sure it's a win. Using a lot of memory can also cost you heavily in speed (due to memory allocations, locality issues, and other costs of dealing with RAM). There's an old CS belief in time/space tradeoffs, but in modern systems there is often no such tradeoff. Saving space can also save time in nonintuitive ways. Profile profile profile.

With records on this scale, wise data structures and good algorithms become really important. I would definitely look at a trie or prefix tree for this kind of structure. In my experience, if this lives in an array, I would lean heavily towards structs. But for list- or tree-like structures, you will generally want classes in Swift.

Related