user3327194
user3327194

Reputation:

Keeping C#'s List<> performance efficient

My application will have Lists. Each object in one list will have to be tied to an object in another list. My idea is to access them by an Index from Lists.

List<MyType1> listNrOne;
List<MyType2> listNrTwo;

foreach (int index in indexes)
{
    someObj.SomeFunc(listNrOne[index], listNrTwo[index]);
}

IN ANOTHER WORDS listNrOne[index] and listNrTwo[index] will be tied to a real life object.

I want to keep the application memory efficient. So I wont let the GC collect the unnecessary objects and INSTEAD REUSE THEM. So:

// The application does not need listNrTen[11]    
queueOfNotNeeded.Enqueue(listNrTen[11]);
listNrTen[11] = null;
// ...
// suddenly an object of the type is needed
// so instead of creating a new one
// the application will reuse existing ones (one that wasn't GCd)
// listNrTen[159] is currently null
listNrTen[159] = queue.Dequeue();

PROBLEM: Will this fragment the memory and how efficient will it stay over time ?

EDIT: The application is a game, so performance is important.

Upvotes: 0

Views: 1335

Answers (5)

supercat
supercat

Reputation: 81123

I would suggest that you may be best off using an array (not a List) of exposed-field structures. Creating the array will allocate space for one structure instance per element, and that space will remain allocated for the lifetime of the array. The time required to read or write an exposed field of a structure stored within an array or an exposed field of another structure, pass a structure as a ref parameter, or read or write an exposed field of a structure received as a ref parameter, are all unaffected of the size of the structure. Provided you only cause structures to be copied in cases where you actually want to copy all the information contained therein, you may ignore any advice suggesting that you avoid structures over 16 bytes.

The .NET advice for designing structures assumes that one is designing structures which will be used like objects. If one is be designing structures which will be used like structures, the rules are completely different: A "structure-style" structure's state should be nothing more nor less than the sum of the contents of its public fields, and each field's meaning should simply be "the last thing somebody wrote to this field". Use instance methods or properties only as needed to interface with other code (e.g. via Equals(Object), IEquatable<T>.Equals(T), GetHashCode(), and ToString()); otherwise use static methods methods which take a structure as a ref parameter.

Arrays of structures offer very good performance in .NET; using an array and a Count, you can do just about anything that could be done with a List<T>; having to resize the array manually may be a slight nuisance, but the ability to operate on structure elements stored within an array is a major performance benefit.

Upvotes: 0

Joel Coehoorn
Joel Coehoorn

Reputation: 415665

Not sure I follow, but pairing lists objects by index is not normally good design. You store the objects as members of a class instead, and keep of single list of that class type. .Net already provides a class you can use to group objects without needing to define a new class just to use with your List: the Tuple.

var Nr = new List<Tuple<MyType1, MyType2>>();

When you have empty spots, you can just create a Tuple where one of the elements is null:

Nr.Add(Tuple<MyType1, MyType2>.Create(null, MyType2Instance));

However, Tuples are only provided for you with size up to 8. I see indication that you need at least 10. So we're likely back to the point where you want to define a custom class, with a field to hold an instance for each of the types you're working with.


I can also say that what you're trying to do with nulls, caching null references to save memory, and creating objects in chunks to save time just will not work. You still need to call new once per object to allocate the space and create the object, and .Net already handles allocating memory in batches for you behind the scenes. Creating objects in chunks of 15 especially is just not a good way to handle this.

My advice here is treat performance as an engineering problem, where you rely on actual measurements to tell you where your code is slow and allocate your programming time that way. In other words, just go build the thing without worrying too much about it at first. Then use a profiler to tell you which code needs tuned to give you the most benefit. It's almost always somewhere other than where you think.


"Looping through objects of the same type is important."

Okay. Using the type/class suggestion, loop through all objects in the Nr list above in the MyType1 position:

foreach( var item in Nr.Where(i => i.Item1 != null).Select(i => i.Item1))
{
    //...
}

But it sounds more and more like you're moving in the completely wrong direction here, by trying to build a data structure that you think will be more efficient for large numbers of objects, rather than trying to build a data structure that maps well to your problem space and efficiently tracks only the relationships among the objects that you need.

Upvotes: 3

Luaan
Luaan

Reputation: 63722

As long as you don't fix the memory, it will not fragment. If you don't use GCHandles and unsafe code, you're probably safe. In .NET, the managed heap is compacted once in a while, which removes memory fragmentation, as long as you don't prevent it (eg. by pinning a handle for an extended period of time).

However, even in a game, I'd be careful about optimizations like this before you actually determine you have a real problem. That said, reusing a List is one of the simple optimizations; however, make sure that you don't allocate a 500 MiB List at one point, and never exceed 10 MiB in common usage - if this does happen, you will want to implement some "compaction", ie. if the List gets too empty, you might want to recreate it without the empty spaces.

The important part is that this only makes sense if you actually have a performance problem related to List allocations. I doubt that is the case. In practice, I expect that initializing the objects themselves will be much slower than anything you might gain from these kinds of micro-optimizations, especially if you don't keep the list almost full most of the time.

Profile the performance. You'll see how much you care. The great thing about managed programming is that it's usually extremely easy to change your mind later in those kinds of micro-optimizations. If you really want, you can hide it behind a very thin abstraction (this pattern looks a lot like a factory - implement it "naively" right away, and it will not eat up much performance (it will probably be optimized away altogether), and if you run into a problem, just change the factory).

Upvotes: 3

Dan Bryant
Dan Bryant

Reputation: 27495

It's worth noting that memory allocation by the CLR is optimized for frequent object allocation, due to the fact that it generally allocates from the top of the heap (very fast), then relies on periodic heap compaction during garbage collection to keep space available. This is different from traditional memory allocation models used by many C++ compilers, for instance, which sometimes have to search the heap to find an empty space for a new object. As such, you may not be saving as much as you think by reusing objects. In fact, you might actually be reducing performance by reducing memory locality and impacting memory caching (the re-used instances will not be nearby in heap memory to other things you've recently allocated, which you often want to use together.)

The moral of the story is that you generally want to start with an approach that is simple and easy to maintain first, then profile once you have it built to identify your true performance bottlenecks. With modern PCs, it's very easy to incorrectly identify the true bottlenecks 'in theory' and wind up minimally improving or even hurting performance with attempted optimizations.

Upvotes: 4

GregRos
GregRos

Reputation: 9103

I recommend you worry about the performance of the implementation much later, and hide all those details underneath an interface. Stick to simple stuff that works for now, and optimize when you see you don't meet your performance goals. You'll also be able to tell what you should optimize, because often the things you think will bottleneck the application turn out to be inconsequential.

In addition, you should be aware that there exists a simple mechanism that allows you to "revive" objects. The primary (and only use case, as far as I'm aware) of this mechanism is an object pool scenario. This isn't normally recommended, but if reconstructing the container does turn out to be a bottleneck, then this is a neater solution than storing it in the fashion you describe.

Here are the relevant links:

Usages of object resurrection

http://blogs.msdn.com/b/abhinaba/archive/2009/04/13/object-resurrection-using-gc-reregisterforfinalize.aspx

Upvotes: 3

Related Questions