July 2010 - Posts
After more than three years wielding my trusty Dell XPS M1210, I felt it was time to upgrade to a Dell Alienware M15x. This is also a history of the laptops I used during the last three and a half years.



- Three and a half years ago I bought a Dell XPS M1210 with an Intel Core 2 Duo CPU @ 1.86 GHz, 2 GB of memory, and a 120 GB 5400 RPM hard drive. This laptop has a 12.1” display.
- Two years ago I bought an Asus Eee 900 with an Intel Celeron processor at @ 900 MHz, 1 GB of RAM, and the slowest 16 GB SSD one could envision (the system never got a WEI because the disk assessment would time out). This laptop has a 9” display.
- A year and a half ago I bought a Dell Latitude XT with a multitouch display (the primary reason why I needed it at the time was for the Windows 7 Training Kit development), as well as an Intel Core 2 Duo ULV CPU, 3 GB of memory, and a 120 GB 5400 RPM hard drive. This laptop has a 12.1” display.
- Less than a year ago I received an Acer Aspire 1420P laptop with an Intel Celeron dual-core U2300 CPU @ 1.2 GHz, 2 GB of RAM, and a 250 GB hard drive. This laptop has an 11.6” display.
The laptop I bought this time is meant to serve me for at least two years. I was having a difficult time choosing between the Alienware M11x, which is the tiniest gaming laptop and closer to my usual comfort range in display sizes, and the Sony Vaio Z which a beautiful machine. I decided to compromise and go for the 15” display but pack the machine full of the best parts available.
The final spec is this:
- Intel Core i7 940XM @ 2.13 GHz (3.33 GHz Turbo Mode)
- 8 GB DDR3 memory
- 15.6” wide 1920x1080 display
- ATI Radeon Mobility HD 5850 (1 GB memory)
- 256 GB Samsung PM800 SSD
- Intel WiFi Link 5300 802.11 AGN card
Admittedly, I haven’t done the final driver tuning yet (I was too busy using this amazing machine, finally being able to demo truly parallel applications from the convenience of my laptop), but at this time I have a WEI of 7.1, with the graphics adapter being the weakest link:
So far, the weight (more than 4kg) hasn’t been a problem for me—I’ve been using my Victorinox backpack into which it fits perfectly.
Battery life isn’t splendid, of course, but there’s not much to be expected from such a beast. I did squeeze more than 2.5 hours from it with WiFi on and without entering Stealth Mode, so it’s not that bad.
In this final installment we’ll take a look at some additional examples of problems caused by distributed transactions.
The following execution history results in a deadlock. What would you suggest to address it?
| Time | Transaction 1 (Inventory Service) | Transaction 2 (Shipping Service) |
| 0 | Select all orders by customer A | |
| 1 | Calling Shipping Service, passing to it each order | Receive order from Inventory service |
| 2 | | Select the customer’s shipping address |
| 3 | | Update the shipping date of the order |
| 4 | Receive execution result | Return from call |
| 5 | Commit | Commit |
The deadlock occurs at T=3 when Transaction 2 attempts to acquire an exclusive lock on a row that is under a shared lock by Transaction 2. To alleviate the deadlock, the Inventory service might want to commit the transaction before calling the Shipping service (why does this help?), or it could flow the transaction into the Shipping service, effectively unifying the two transactions into one.
Let’s take a look at a more complicated example. In the following execution history, the Inventory WCF service is configured with ConcurrencyMode.Single. The execution history results in a deadlock.
| Time | Transaction 1 (Order Service) | Transaction 2 (Shipping Service) | Inventory Service (within caller’s TX) |
| 0 | Request in-stock information for an item from the Inventory Service | | Receive request from caller |
| 1 | | | Select in-stock information for the item |
| 2 | Receive reply from service | | Return to caller |
| 3 | | Select all orders | |
| 4 | | Update in-stock information in Inventory Service | Receive request from caller |
| 5 | | | Update in-stock information for the items in the order |
| 7 | Update in-stock information in Inventory Service | | |
| | DEADLOCK | DEADLOCK | |
Note that what happens is the following: at T=1, the Inventory service acquired a shared lock on a certain row on Transaction 1’s behalf. At t=4, the Inventory service starts processing a request on Transaction 2’s behalf, but it requires an exclusive lock on the same row that’s currently under a shared lock by Transaction 1. Therefore, the Inventory service blocks. At T=7, the Order service needs to access the Inventory service again, but because of the use of ConcurrencyMode.Single, it can’t get into the service—the Shipping service is already inside. This forms again a deadlock.
The Suspend-Enqueue-Unload-Resume pattern for smuggling transactional work into a WF instance introduces a similar transactional deadlock. [I won’t repeat myself—this has been discussed in an earlier post.]
Finally, a fairly common error when working with distributed transactions occurs when you inadvertently attempt to use a transaction that has already been aborted for some reason.
One place where this can happen is if you call two WCF service operations within the same distributed transaction, and the first operation faults (aborting the transaction). Trying to flow the transaction into the second operation will cause an exception.
Another cause for this error could be a timeout. Your thread might have blocked for a long time, and after waking up it would attempt to use the ambient transaction. If it were already aborted (due to the transaction timeout), an error will occur.
It’s time to add WCF services to the mix and see how their presence adds to the complexity of the otherwise non-trivial transactional deadlocks. Consider the following transaction:
- Client: Begin transaction
- Client: Select all orders by customer A
- Client: Call a WCF service and pass to it the list of orders
- Service: Update the shipping date of all selected orders
- Client: Commit
If both parties share the same distributed transaction, it completes successfully. However, if the transaction flow between the client and the service were not properly configured (see earlier post), then a deadlock would occur. There would be two transactions—the service-side transaction would attempt to acquire an exclusive lock on rows that are under a shared lock by the client’s transaction, while the client’s transaction won’t commit until the (synchronous) service call returns.
In this case, by the way, the database access attempted by the service will likely yield the following exception:
SqlException: Timeout expired. The timeout period elapsed prior to the completion of the operation of the server is not responding.
In other words, the database does not detect the deadlock in this case—and it’s not very surprising, considering that only one part of the deadlock is actively accessing the database while it’s happening. [SQL Server Profiler can even plot for you the database deadlock as a wait graph. This is very similar to the operating system’s Wait Chain Traversal.]
One tool for diagnosing transactional deadlocks involving an SQL Server database is the SQL Server Activity Monitor that you can access from the SQL Management Studio. It displays all the transactions that are currently in flight, and can tell you which locks (shared or exclusive) they acquired as well as whether they are currently blocked waiting for another transaction.
Here’s a screenshot from the Activity Monitor that I captured while two transactions entered a mutual deadlock:
Right-click any process and choose Details, and you can see the last query that the process is trying to perform:
Sometimes, you would see –2 as the blocking transaction identifier. It means that the transaction is blocked waiting for a distributed transaction to commit, but that other distributed transaction is no longer connected to the database (so it doesn’t have a database process ID).
This can happen if you’re performing multiple database accesses within a single transaction, or if the transaction performs a database access and then blocks for another reason, as in the following example:
| Time | Transaction 1 | Transaction 2 |
| 0 | Open session to CRM DB | Open session to CRM DB |
| 1 | Select all orders from customer A from the CRM DB | |
| 2 | Update the shipping date of all selected orders | |
| 3 | Close session to CRM DB | |
| 4 | Open session to Inventory DB | Select all orders from customer A from the CRM DB |
| 5 | Select inventory data from the Inventory DB | |
| 6 | . . . | |
In this case, Transaction 2 would block at T=4, even though Transaction 1 closed its session to the CRM DB. The session might be closed, but until the distributed transaction commits, the locks are still held.
Question: Suggest an operation that Transaction 1 would perform at T=6 so that it would become deadlocked with Transaction 2. (Hint: This does not have to be a database operation.)
Answer: One possibility is to enter a wait for an event that the thread executing Transaction 2 is supposed to signal when it completes.
If the Activity Monitor reports –2 as the blocking transaction identifier, you can use the Microsoft Distributed Transaction Coordinator (MSDTC) MMC snap-in to view the currently active distributed transactions. [It’s under Administrative Tools –> Component Services –> Computers –> My Computer –> Distributed Transaction Coordinator.]
Inspecting the transaction’s distributed transaction identifier (shown here as the Unit of Work ID) might be sufficient to go back to the logs and see what the transaction is doing.
One thing that might help you with diagnostics of that sort is having a trace statement output the transaction identifier, stack trace, and SQL statement whenever you create a new transaction and whenever you use a transaction to access the database. There are many places where you might want to add such tracing information to your code, so I’ll leave it up to you to decide what’s best. [For example, if you’re using LINQ to SQL, you can implement a TextWriter that outputs the transaction information and provide it to the DataContext.Log property.]
In the next (and final) installment, we’ll see some additional examples of problems that arise from incorrect use of distributed transactions.
The most commonly encountered problem with distributed transactions is that of transactional deadlocks. Transactions guarantee isolation, which is usually effected through locks. This is the case with SQL Server (as well as other relational databases) transactions. Transaction isolation levels provide additional granularity as to when and whether the locks are released prior to the completion of the transaction.
Database Locks
Simply put, the database issues shared (read) and exclusive (write) locks on data touched by the transaction. For most practical purposes, a SELECT statement effects a shared lock on all the rows returned by the statement, while an UPDATE statement effects an exclusive lock on all the rows affected by the statement.
The following is a simple example of a deadlock caused by locks between distributed transactions:
| Time | Transaction 1 | Transaction 2 |
| 0 | Select all orders from customer A | |
| 1 | | Select all orders from customer A |
| 2 | Update the shipping date of all selected orders | |
| 3 | | Update the discount rate of all selected orders |
| 4 | Commit | Commit |
Question: At which time will the system encounter the deadlock? Which transaction will be the first to block?
Answer: At time T=2. Transaction 1 will block while attempting to update (i.e., lock for writing) records that are currently under a shared lock (for reading) by Transaction 2. Similarly, Transaction 2 will block while attempting to update records that are currently under a shared lock by Transaction 1. Therefore, the two transactions would wait for each other.
This form of deadlock is resolved by the database itself. SQL Server detects that the two transactions are deadlocked on the same resources, and chooses one of them as the transaction victim. The victim transaction is aborted—your application will receive an exception similar to the following:
SqlException: Transaction (Process ID ...) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
Lock Query Hints
You can explicitly specify the desired lock behavior for your SELECT statement by using the XLOCK and NOLOCK query hints. If you’re using an ORM (such as LINQ to SQL), you’ll have to find a way to add these hints to the query issued by the ORM classes, or issue the query yourself and have the ORM materialize the results into objects.
In the previous example, if the SELECT query used by the transactions were to effect an immediate exclusive lock, the deadlock would not be possible. One of the transactions would acquire the lock first, and proceed to completion while the other would wait. Indeed, the XLOCK query hint can instruct the database to acquire an exclusive lock:
SELECT * FROM Customers WITH (ROWLOCK, XLOCK)
WHERE Name = 'A'
As a result, the following execution history will be obtained:
| Time | Transaction 1 | Transaction 2 |
| 0 | Select all orders from customer A with XLOCK | |
| 1 | Update the shipping date of all selected orders | |
| 2 | Commit | |
| 3 | | Select all orders from customer A with XLOCK |
| 4 | | Update the discount rate of all selected orders |
| 5 | | Commit |
In this case, the first transaction to acquire the exclusive lock commits first.
Similarly, the NOLOCK query hint instructs the database to acquire no locks (accepting the possibility of reading dirty data that might be modified by another transaction). This query hint should be used with great caution, because you might rely on data that is about to be changed, or data that has been changed but the change will be rolled back should the transaction that caused the change abort.
Question: In the following execution history, is it necessary to introduce a NOLOCK query hint to the query executed by Transaction 2 at T=2 to ensure that there is no deadlock?
| Time | Transaction 1 | Transaction 2 |
| 0 | Select all orders from customer A | |
| 1 | Update all selected orders by marking them as FAILED TO DELIVER | |
| 2 | | Select all orders marked as FAILED TO DELIVER |
| 3 | Commit | Commit |
| 4 | | Dispatch a customer service representative to all customers who have the orders selected earlier |
Answer: There’s no need to use the NOLOCK query hint. Transaction 2 performs only reads, and therefore cannot in any way become deadlocked with Transaction 1. The operation at T=2 might wait until T=3 when Transaction 1 commits.
Indexes
Database indexes have a considerable effect on the locks acquired by transactions. In the previous example, if there is an index on customer names, the SELECT statement would lock only the orders by customer A. Otherwise, the following two transactions would deadlock, even though they are not actually working on the same rows:
| Time | Transaction 1 | Transaction 2 |
| 0 | Select all orders from customer A | |
| 1 | | Select all orders from customer B |
| 2 | Update the shipping date of all selected orders | |
| 3 | | Update the discount rate of all selected orders |
| 4 | Commit | Commit |
In the next installment, we’ll see how transactional deadlocks involving also WCF services can be detected with SQL Server Activity Monitor, the MSDTC MMC snap-in, and some diagnostic code.
How is a WCF service call under a distributed transaction different from a regular WCF service call? The primary difference is the transactional abort semantics in case of failure.
If the WCF service operation fails when there is no ambient transaction, the failure can be contained—the service caller may decide to ignore the error and proceed. However, if the WCF service operation fails within a distributed transaction, the transaction’s abort flag is set (by default), so that the caller cannot proceed with the transaction. In other words, if you handle a fault from a transactional WCF service and want to swallow the error and continue working, you will need a new transaction—the old transaction will have already been aborted.
This mistake (that stems from the fact the transaction is automatically aborted) is demonstrated by the following failing test:
[TestMethod]
public void ExceptionHandledWhenServiceFails()
{
IBookStore store = ObtainBookstore();
using (TransactionScope tx = new TransactionScope())
{
try
{
store.PromoteCustomerToVIP("@$!$!@#!");
Assert.Fail("Exception expected.");
}
catch (FaultException)
{
}
tx.Complete();
}
}
The problem here is that because the transaction has already been aborted, the tx.Complete() call will throw an exception. Here’s the right way to write this test:
[TestMethod]
public void ExceptionHandledWhenServiceFails_Right()
{
IBookStore store = ObtainBookstore();
try
{
using (TransactionScope tx = new TransactionScope())
{
store.PromoteCustomerToVIP("@$!$!@#!");
tx.Complete();
}
Assert.Fail("Exception expected.");
}
catch (FaultException)
{
}
}
In the next installment, we’ll start looking at transactional deadlocks—the plague of distributed transactions.
This is the first in a short series of posts in which we’ll debug problems that arise from the use of distributed transactions in WCF. Particularly, we will look into deadlocks, timeouts, and miscellaneous transaction-related issues.
To begin with, here’s a brief recap of what has to be done to flow a transaction across WCF service calls:
- The WCF operation must be decorated with the [TransactionFlow] attribute and the TransactionFlowOption enum. You can either disallow, allow, or mandate transaction flow to the service operation—violations of the contract result in a ProtocolException on the caller’s side.
- The .NET method implementing the operation contact must be decorated with the [OperationBehavior(TransactionScopeRequired=true)] attribute.
- The WCF binding used by both parties must support transaction flow, and have its TransactionFlow property set to true (this enables the transaction flow binding element).
- If the .NET class that implements the service contract is configured with ConcurrencyMode.Multiple and InstanceContextMode.Single, then it must also be configured with ReleaseServiceInstanceOnTransactionComplete=false.
The following is a complete example of a service implementation that supports (but does not mandate) transaction flow:
[ServiceContract]
public interface IBookStore
{
[OperationContract]
[TransactionFlow(TransactionFlowOption.Allowed)]
void PlaceOrder(OrderDTO orderDTO);
}
[ServiceBehavior(
InstanceContextMode = InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Multiple,
ReleaseServiceInstanceOnTransactionComplete = false)]
class BookStore : IBookStore
{
[OperationBehavior(TransactionScopeRequired = true)]
public void PlaceOrder(OrderDTO orderDTO)
{
using (CrmDataContext context = DataContextFactory.GetContext())
{
context.Orders.InsertOnSubmit(
new Order
{
OrderId = Guid.NewGuid(),
Amount = orderDTO.Amount,
Item = orderDTO.Item,
CustomerName = orderDTO.CustomerName,
DiscountRate = 0.0f
});
context.SubmitChanges();
}
}
}
To verify that the transaction flows from the client to the service, you can use the WCF Service Trace Viewer after configuring tracing. Alternatively, if you’re still in the debugging phase, look at the Transaction.Current static property in the Immediate Window on both sides (the service and the caller) and make sure that the transaction information is the same.
The TransactionInformation property has a local transaction identifier and a distributed transaction identifier. If the transaction was properly flown across the WCF service call, it will always have a distributed transaction identifier. By the way, transactions exhibit the same auto-promotion behavior when flown across AppDomains, if the Transaction instance is marshaled by the Remoting infrastructure.
Stay tuned for more installments in which we will begin to diagnose some errors caused by the nature of distributed transactions.
Inside a method that is generic in T you may invoke on an expression of type T any of the System.Object methods, or methods that belong to the class or interface constraints of T. Because the JIT does not invoke a full-blown compiler when generating code for a specific type T, there is a significant difference in performance when invoking methods on value types in this fashion.
Assume the following value type describes a point in two-dimensional space. The value type overrides ValueType.Equals and provides an overload with the same name, as per the value type best practices:
struct Point
{
public int X, Y;
public override bool Equals(object obj)
{
if (obj == null)
return false;
if (!(obj is Point))
return false;
Point other = (Point) obj;
return Equals(other);
}
public bool Equals(Point other)
{
return X == other.X && Y == other.Y;
}
}
The following code is functionally correct, but invokes the “wrong” version of Point.Equals when used with T=Point:
class Collection<T>
{
private readonly T[] _elements = ...;
public bool Contains(T elem)
{
foreach (T inst in _elements)
{
if (inst.Equals(elem))
return true;
}
return false;
}
}
How do I know that? To begin with, I could place a breakpoint inside the two Equals methods and see which one was invoked. Alternatively, I could fire up WinDbg and look at the JITted code:
D:\Scratch\CallingGenericMethod\Program.cs @ 25:
003d02c5 b9d0382900 mov ecx,2938D0h (MT: CallingGenericMethod.Point)
003d02ca e8511debff call 00282020 (JitHelp: CORINFO_HELP_NEWSFAST)
003d02cf 8945d4 mov dword ptr [ebp-2Ch],eax
003d02d2 8d45f0 lea eax,[ebp-10h]
003d02d5 8945d0 mov dword ptr [ebp-30h],eax
003d02d8 8b7dd4 mov edi,dword ptr [ebp-2Ch]
003d02db 83c704 add edi,4
003d02de 8d7508 lea esi,[ebp+8]
003d02e1 f30f7e06 movq xmm0,mmword ptr [esi]
003d02e5 660fd607 movq mmword ptr [edi],xmm0
003d02e9 8b45d4 mov eax,dword ptr [ebp-2Ch]
003d02ec 8945cc mov dword ptr [ebp-34h],eax
003d02ef 8b4dd0 mov ecx,dword ptr [ebp-30h]
003d02f2 8b55cc mov edx,dword ptr [ebp-34h]
003d02f5 ff15a4382900 call dword ptr ds:[2938A4h] (CallingGenericMethod.Point.Equals(System.Object), mdToken: 06000004)
003d02fb 8945e0 mov dword ptr [ebp-20h],eax
003d02fe 837de000 cmp dword ptr [ebp-20h],0
003d0302 0f94c0 sete al
003d0305 0fb6c0 movzx eax,al
003d0308 8945e4 mov dword ptr [ebp-1Ch],eax
Note the boxing operation in the first two lines, which is also reflected in the method’s IL (from Reflector):
L_001c: ldloca.s inst
L_001e: ldarg.1
L_001f: box !T
L_0024: constrained !T
L_002a: callvirt instance bool [mscorlib]System.Object::Equals(object)
Also note that the method is not dispatched virtually, because the JIT can convince itself that the value type is sealed.
So what, you may ask. Well, unfortunately, to call the virtual Point.Equals method the parameter must be boxed, incurring a memory allocation for each iteration of the loop. This will cost us dearly.
What can we do, then? We could use the IEquatable<T> interface as a generic constraint on T, and try using the Equals method from that interface. In other words, the Point struct should now implement the IEquatable<Point> interface and the Collection<T> class should have a constraint “where T : IEquatable<T>” on its generic type parameter. Now, the IL code does not contain a “box” instruction, and the JITted code changes accordingly to this:
D:\Scratch\CallingGenericMethod\Program.cs @ 25:
003d02c2 8d4508 lea eax,[ebp+8]
003d02c5 83ec08 sub esp,8
003d02c8 f30f7e00 movq xmm0,mmword ptr [eax]
003d02cc 660fd60424 movq mmword ptr [esp],xmm0
003d02d1 8d4df0 lea ecx,[ebp-10h]
003d02d4 ff15b8382b00 call dword ptr ds:[2B38B8h] (CallingGenericMethod.Point.Equals(CallingGenericMethod.Point), mdToken: 06000005)
003d02da 8945e0 mov dword ptr [ebp-20h],eax
003d02dd 837de000 cmp dword ptr [ebp-20h],0
003d02e1 0f94c0 sete al
003d02e4 0fb6c0 movzx eax,al
003d02e7 8945e4 mov dword ptr [ebp-1Ch],eax
Now there’s no boxing—the value type is copied onto the stack (using movq) and the method is invoked directly. It is also the “right” method—the Equals(Point) overload and not the Equals(Object) override.
Does it really matter, performance-wise? Indeed it does. I just ran a quick test calling the Contains method a 10,000 times in a row on a collection with 10,000 points, looking for an element that isn’t there (in other words, invoking the Equals method for each element). With the IEquatable<T> constraint, it took 97ms. Without the constraint, it took 941ms.
While these results should be taken with a grain of salt, there’s an order of magnitude difference here. Not negligible at all.
I read a couple of posts by Simon Cooper where he explains in great details why mutable structs are bad, and how the BCL’s enumerator structs (e.g. LinkedList<T>.Enumerator) are prime candidates for this anti-pattern. He dissects a really nasty bug where declaring a value type field as readonly effects unexpected copy semantics if the value type is modified. The following code enters an infinite loop (which prints 0 indefinitely):
class EnumeratorWrapper
{
private readonly LinkedList<int>.Enumerator _enumerator;
private readonly LinkedList<int> _list;
public EnumeratorWrapper()
{
_list = new LinkedList<int>(Enumerable.Range(0,5));
_enumerator = _list.GetEnumerator();
PrintAll();
}
private void PrintAll()
{
while (_enumerator.MoveNext())
{
Console.WriteLine(_enumerator.Current);
}
}
}
The reason is that if you declare the field readonly, the MoveNext call will be called on a copy of the field’s value, to prevent it from being modified. Admittedly, this seems like enough of a reason to ban mutable value types altogether, and the enumerator structs in particular.
So why bother with a value type? As the matter of fact, why bother exposing the type that implements the IEnumerator<T> interface, and not declare it a private class and return only the interface? In other words, what’s the difference between the following two approaches?
class MyCollection : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator()
{
return new MyPrivateEnumerator();
}
private class MyPrivateEnumerator : IEnumerator<int>
{
//Implementation omitted
}
}
class MyOtherCollection : IEnumerable<int>
{
public MyEnumerator GetEnumerator()
{
return new MyEnumerator();
}
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
return GetEnumerator();
}
public struct MyEnumerator : IEnumerator<int>
{
}
}
Well, as I (and many others) have written elsewhere, calling a method through an interface reference is more expensive than calling it directly. The primary motivation behind publicly exposing the enumerator type is that clients of the collection class can use it directly, without going through an interface reference.
Fortunately, the foreach statement uses duck typing to perform the enumeration on the collection—and the foreach statement is really the common case of using any enumerator at all. This means that in the following fragment, the public MyOtherCollection.GetEnumerator method is called, and the generated code uses the enumerator struct directly, and not through an interface reference:
MyOtherCollection collection = new MyOtherCollection();
foreach (int i in collection)
{
Console.WriteLine(i);
}
Roughly equivalent to:
var enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
int i = enumerator.Current;
Console.WriteLine(i);
}
That’s why there are no method calls through an interface here—the “var” is MyOtherCollection.MyEnumerator and not IEnumerator<int>! Moreover—and this is the critical point—if the enumerator implementation were to go through the same hoops of implementing the interface explicitly and at the same time providing public methods that do the same thing, then the MoveNext and get_Current method calls would also be direct calls (not through an interface reference), and therefore could be inlined. Obviously, the only chance of ever achieving performance with a foreach block over a collection that is comparable to that of a built-in array is if the collection accessors are inlined. And it’s very simple, indeed:
public struct MyEnumerator : IEnumerator<int>
{
public bool MoveNext()
{
//Actual implementation here
}
bool IEnumerator.MoveNext()
{
return MoveNext();
}
void IEnumerator.Reset()
{
throw new NotImplementedException();
}
public int Current
{
get { /* Actual implementation here */ }
}
object IEnumerator.Current
{
get { return Current; }
}
}
Now, if the caller is working with a variable of type MyEnumerator, they would access the critical methods directly, without an interface call, allowing for inlining to take place (not to mention that the integer will not be boxed when returning from the get_Current call). Still, if someone insists on using an interface to perform the enumeration, we implement our part of the contract using explicit interface implementation.
Finally, why bother with the value types? The probable reason is to avoid object allocations. Allocating a value type and returning it is significantly cheaper than allocating a heap object and reclaiming it later. Although this is not the critical performance improvement here (the explicit implementation trick is the crucial part), it’s still worthwhile.
Is it worthwhile, though, to introduce a mutable value type, which is a big no-no by all accounts, for this performance gain? I think it is. If the enumerator type were commonly used on its own, I wouldn’t agree—but this particular enumerator type is used, 99.99% of the time, in a foreach statement, where the developer is not exposed at all to its existence.
I think that implementing a mutable value type that’s invisible in the vast majority of development scenarios and that provides a non-negligible performance gain is an acceptable compromise.