-1

We're pushing log items of type LogEntry onto a list of type List<LogEntry> that'll be saved to file at a much later time.

EDIT 1: The reason we don't flush the log to file immediately is that this is in a highly multi threaded app on Windows Phone with isolated storage. Writing to isolated storage is inherently slow, miles away from desktop performance. The overhead of flushing every message immediately actually kills the concurrent state transitions and interactions we want to observe with the logs. END EDIT 1

Assuming we add millions of items to the list during a not too long time interval, would we be better off using a value or reference type for the item, given the below content of the item?

I.e.

internal struct LogEntry
{
    public int ThreadId { get; private set; }
    public DateTime Timestamp { get; private set; }
    public string Message { get; private set; }

    public LogEntry(int threadId, DateTime timestamp, string message)
        : this()
    {
        this.ThreadId = threadId;
        this.Timestamp = timestamp;
        this.Message = message ?? string.Empty;
    }
}

or

internal sealed class LogEntry
{
    public int ThreadId { get; private set; }
    public DateTime Timestamp { get; private set; }
    public string Message { get; private set; }

    public LogEntry(int threadId, DateTime timestamp, string message)
    {
        this.ThreadId = threadId;
        this.Timestamp = timestamp;
        this.Message = message ?? string.Empty;
    }
}

Before actually saving the log to file, we're not doing anything to the item list other than adding items. We don't search, remove or enumerate it - just add items.

What say you, does struct or class really matter?

EDIT 2: The result of measuring the timing of adding 60000 entries on the UI thread interleaved by queuing in thread pool work items was 65 seconds for the struct case and 69 seconds for the class case. Since this difference is small and the class implementation is slightly cleaner and I have no need for value equality semantics, I've decided to go for the LogEntry as a class. END EDIT 2

Johann Gerell
  • 24,991
  • 10
  • 72
  • 122
  • 2
    Why don't you profile it? This can be done simply using the Stopwatch class. – Polyfun Feb 24 '12 at 16:19
  • exact duplicate: http://stackoverflow.com/questions/5432681/performances-of-structs-vs-classes – vulkanino Feb 24 '12 at 16:20
  • 5
    Frankly I'd get them down to file ASAP. A log isn't any use if it is still in-memory when the app crashes. – Marc Gravell Feb 24 '12 at 16:21
  • 12
    You have two horses; you wish to know which is faster. Do you (1) show a picture of the horses to strangers on the internet and have them guess which one is faster, and by how much, or (2) race the horses against each other? – Eric Lippert Feb 24 '12 at 16:46
  • @Marc: This is in a highly multi threaded app on Windows Phone with isolated storage. Writing to isolated storage is inherently slow, miles away from desktop performance. The overhead of "writing immediately" actually kills the concurrent state transitions we want to observe with the logs. I should have stated this more clearly in the question. – Johann Gerell Feb 24 '12 at 21:31
  • 1
    @Eric: The **combination** of simply profiling the case **and** see the multitude of answers **with** comments says more about **why** things are as they are then just profiling would have. For instance, I have profiled it and timewise seen hardly any difference, then it'd be good to know if there could be other ramifications of this that makes one better suited for our situation than the other. – Johann Gerell Feb 24 '12 at 21:48
  • 1
    @vulkanino: Oh, gosh, please... They're almost not even related. – Johann Gerell Feb 24 '12 at 21:49

4 Answers4

1

If you go with a class, every storage location (variable, struct field, or array element) of that class type will be 4/8 bytes (on x86/x64), and every distinctly-created instance of that type which at least one reference exists will take 16/20 bytes for the fields plus an additional 8/16 bytes of class-related overhead. If you go with a struct, every storage location of that struct type will take 16/20 bytes, period. Unless you expect to that many instances of your type will have multiple storage locations pointing to them, a struct is going to be more efficient. When using so-called immutable structs, one should be aware that the statement:

  myLogEntry = new LogEntry(someId, someTime, someMessage)

will actually be executed in C# as

  LogEntry temp;
  LogEntry.CallConstructor(ref temp, someId, someTime, someMessage); // not the real method name
  myLogEntry._ThreadId = temp._ThreadId;
  myLogEntry._Timestamp = temp._TimeStamp;
  myLogEntry._Message = temp._Message;

which is to say that the constructor will generate a new temporary instance, and then the assignment will mutate the existing instance by copying all public and private fields from the temporary one (struct assignment nearly always works by effectively copying all public and private fields). In most single-threaded code this won't be an issue, but in multi-threaded code it's important to note that reading a struct while it is being written may yield an arbitrary mix of old and new data. I prefer mutable structs, in some measure because writing fields directly makes it more obvious that they are, in fact, being written individually.

supercat
  • 77,689
  • 9
  • 166
  • 211
1

The result of my measurements of the timing of adding 60000 entries on the UI thread interleaved by queuing in thread pool work items was 65 seconds for the struct case and 69 seconds for the class case. Since this difference is small and the class implementation is slightly cleaner and I have no need for the default value equality semantics given by struct, I've decided to go for the LogEntry as a class

Johann Gerell
  • 24,991
  • 10
  • 72
  • 122
0

at the start of the application, the app memory print is quite minimal in both cases

if you go the stack (structure) path,

  1. at start up, you will have an instance of the struct loaded at startup.
  2. in the course of time, you append log entries to this structure
  3. -- Memory print grows
  4. you commit this to file
  5. --you clear contents of the structure (I am presuming)
  6. the structure will remain on the stack

if you go the heap (class) path

  1. at app startup, there is one less object instantiated
  2. on the first use of the class, you instantiate this object
  3. you add log entries to this class
  4. --Memory print grows, (this will be the same rate)
  5. you commit the contents to the file
  6. and de-reference this object and let garbage collector to do its job

the only and big (- I believe) difference is the retention of memory resources in the applications life time

Also Eric did a great job explaining the garbage collection on the value types

Community
  • 1
  • 1
Krishna
  • 2,451
  • 1
  • 26
  • 31
  • Why would the structure remain on the stack? A storage location of structure type will hold the struct's fields (public and private). If the storage location itself is on the stack, the struct's fields will go on the stack. If the storage location is a class field, the struct's fields will go on the heap *as part of the enclosing object*. If it's a struct field, the inner struct's fields will go wherever the outer struct goes. – supercat Feb 24 '12 at 19:04
-1

Short answer: I'd go for the class.

As a value type, a struct, when created in "local scope" (specific to one class or method), is usually placed on the stack. This allows for fast access, and it's easy to get rid of once you no longer need it, so it's the go-to way to store simple, locally-scoped variables. However, in your case, this value type will become an element of a collection (which is a reference type, stored on the heap). Objects on the heap cannot reference objects on the stack; the object must be copied from the stack to the heap, and a reference to the object's new heap address made. This process is called "boxing". Then, when the struct is assigned to another local variable, or passed by value into a method, it is copied back to a location on the stack ("unboxing"). This process of boxing and unboxing will affect performance, and is generally to be avoided where feasible (of course there are times you simply can't avoid it; a List of a value type is not a bad thing just because it requires boxing, if it's the right tool for the job).

A class, by contrast, is a "reference type", and when created it is stored on the heap by default. This means it can hang around as long as necessary (as long as it has at least one reference made to it by other variables in scope), and all that needs to be on the stack is a 32-bit integer "pointer" (technically not the same as a C/C++ pointer, but similar) to the object's address in the heap. To access an object's data members, the pointer must be "dereferenced" to read the necessary data from the object in the heap, but for the most part while the class isn't actively being manipulated, it's much easier to pass around (a single 32-bit variable instead of a "wide" multi-variable chunk being copied around in various places).

So, since you know the object has to go on the heap no matter what, and in the case of log items you probably won't be doing a large amount of manipulation between creating the object and persisting its data to a file, I think you'll find the class will perform better. But, stack vs heap is frankly an implementation detail that is generally but not always true, and depending on the size of the struct and how many times you pass it around, the overhead of copying from stack to heap one time versus multiple pointer dereferences to initialize the class, either one might work better depending on your exact code. So, I agree with Eric; line the two candidates up in unit tests and may the best implementation win.

KeithS
  • 70,210
  • 21
  • 112
  • 164
  • 2
    Why would a list of a value type require boxing? An array of value types? Or value type members of a class, are those boxed, too? No! That's not what boxing is. Values can be stored on the heap without being boxed. – Anthony Pegram Feb 24 '12 at 18:29