Benefits of having both specific arguments and params method overloads in C#
Asked Answered
C

2

4

There are a number of examples in the .NET framework where there are multiple overloads for a method, some of which use a specific number of parameters followed by a final "catch all" where the params keyword is used. Common examples of this are on the String class e.g.:

I was wondering if there is a particular reason for why there are so many of these method overloads? At first I thought it might be something to do with performance; the question and answers to this SO question - Cost of using params in C# - would suggest so.

However, I started to delve into the .NET source code using the Reference Source website. I noticed this in the String class source code:

String.Concat() actually runs different code depending on how many fixed arguments are used - this in my mind would definitely be an optimization. String.Format() however just seems to provide wrappers around the main param method - see below for the paraphrased code:

public static String Format(String format, Object arg0)
{
    return Format(format, new Object[] { arg0 });
}

public static String Format(String format, Object arg0, Object arg1)
{
    return Format(format, new Object[] { arg0, arg1 });
}

public static String Format(String format, Object arg0, Object arg1, Object arg2)
{
    return Format(format, new Object[] { arg0, arg1, arg2 });
}

public static String Format(String format, params Object[] args)
{
    // Do work here...
}

So are there performance benefits or can this simply be a matter of convenience, or maybe both? In the particular case above I do not see any obvious benefit and it just seems to duplicate work.

Carmeliacarmelina answered 5/9, 2014 at 15:39 Comment(12)
It saves a few bytes of IL in each caller: no need to allocate and initialize an array.Vela
@MichaelLiu But it is allocating and initializing the array here, it's just doing it inside a new method instead of outside.Twofaced
@Servy: It saves a few bytes of IL in each caller. That is, at each call site, the compiler need only emit instructions to push arguments. If there were no dedicated overloads, the compiler would also need to emit instructions to allocate and initialize an array.Vela
The String.Format wrappers seem to violate the .NET Framework Design Guidelines, which state: "CONSIDER providing special overloads and code paths for calls with a small number of arguments in extremely performance-sensitive APIs. This makes it possible to avoid creating array objects when the API is called with a small number of arguments. ... You should only do this if you are going to special-case the entire code path, not just create an array and call the more general method."Vela
@MichaelLiu MS violates their own guidelines constantly. Sometimes it's for good reason (after all, they are guidelines, not rules), sometimes it's not.Twofaced
I suspect it is a legacy from previous framework versions. Maybe earlier versions of .Net actually had different code all the way, then a new implementation in a later version was created only for the params version and the other overloads kept for compatibility reasons (removing overloads would be a very breaking change)Numberless
@Numberless All of the overloads were added in .NET 2.0.Twofaced
@Twofaced : MS guidelines are always: do this until you know really well what you are doing violating this guideline. If you follow MS guidelines, it's far less likely that something horrible will happen to you. But if you really know what you're doing, you can in very specific cases make substantial enhancements to your code by violating a few guidelines.Numberless
@Sevy : Yes, the overloads were added in .Net 2.0. But the sources from Reference Source are for .Net 4.5.1. I expect quite a lot of implementations to have changed between the two iterations, and we don't have the source code for earlier versions.Numberless
@Falanwe: These overloads are in the SSCLI, which was based on .NET 2.0.Vela
@Numberless Yes, which is why stating that the guidelines are violated, in isolation, doesn't make the code wrong. The question is simply whether or not there's a good reason for violating the guideline, which is the whole point of this question.Twofaced
The source code shows string.Format creates a struct not an array for call to format with only 1 2 or 3 arguments. This is exactly what the guideline states. "not just the array". Most likely the struct outperforms the array.Ribbonwood
P
1

Update: Its not String.Concat but String.Format.

I guess the reason behind this is that all calls end up in StringBuilder.AppendFormat which is quite complex and it would be a major source of code duplication if you would handle every number of input arguments in a different way.

Things are different if you have an heavy duty API (like tracing) where the overhead of calling a method via a params overload is very significant. It can be up to a factor of 5. The same goes for implicit delegate allocations which is common with LINQ. These temp delegate instances are not cheap in optimized code. Roslyn for example has banned LINQ usage because of the high implicit delegate allocation costs.

Peterman answered 5/9, 2014 at 15:57 Comment(4)
If it was deemed too much work to actually implement each different overload then why not just not make the overloads to begin with?Twofaced
If you provide already all overloads you can change the implementation later if someone comes up with a great idea. If you would provide these overloads later you would need to recompile every application to make use of the improved overloads.Peterman
Thank you @AloisKraus, this seems to be the closest I was expecting to an answer. Could you explain why params is worse though? e.g. if I had both String.Format("{0},{1}", 1, 2) and String.Format("{0},{1}", new object[] { 1, 2 }) are these exactly the same or is there a difference in these calls too?Carmeliacarmelina
@Peter: There is no difference. They both allocate an array for the arguments. If you have e.g. a method which receives arguments but rarely formats the Input string (e.g. tracing) the differences are substantial. In a large real world application I could improve one central use case by 15% only by adding the right overloads.Peterman
T
2

It allows you to create delegates of each of those signatures that invoke that method:

Func<string, object, object> foo = string.Format;

This wouldn't work with a params method; you could only assign that overload to Func<string, object[]> or a delegate that specifically provided params in its signature. You'd be forced to create a new method that just called the params overload to create a delegate of the signature used int he example (possibly through the use of a lambda).

Twofaced answered 5/9, 2014 at 15:51 Comment(4)
While dedicated overloads make this possible, I doubt this is the actual reason the .NET team created dedicated overloads in this particular case.Vela
Does the params keyword really stop you creating a delegate? I would have assumed it would just be something like Func<string, object[]>?Persuasive
@Persuasive For a params method that is the only type of delegate you could create. You couldn't assign it to a Func<string, object, object>.Twofaced
@Twofaced Yeah, I guess your clarification makes my comment moot now :)Persuasive
P
1

Update: Its not String.Concat but String.Format.

I guess the reason behind this is that all calls end up in StringBuilder.AppendFormat which is quite complex and it would be a major source of code duplication if you would handle every number of input arguments in a different way.

Things are different if you have an heavy duty API (like tracing) where the overhead of calling a method via a params overload is very significant. It can be up to a factor of 5. The same goes for implicit delegate allocations which is common with LINQ. These temp delegate instances are not cheap in optimized code. Roslyn for example has banned LINQ usage because of the high implicit delegate allocation costs.

Peterman answered 5/9, 2014 at 15:57 Comment(4)
If it was deemed too much work to actually implement each different overload then why not just not make the overloads to begin with?Twofaced
If you provide already all overloads you can change the implementation later if someone comes up with a great idea. If you would provide these overloads later you would need to recompile every application to make use of the improved overloads.Peterman
Thank you @AloisKraus, this seems to be the closest I was expecting to an answer. Could you explain why params is worse though? e.g. if I had both String.Format("{0},{1}", 1, 2) and String.Format("{0},{1}", new object[] { 1, 2 }) are these exactly the same or is there a difference in these calls too?Carmeliacarmelina
@Peter: There is no difference. They both allocate an array for the arguments. If you have e.g. a method which receives arguments but rarely formats the Input string (e.g. tracing) the differences are substantial. In a large real world application I could improve one central use case by 15% only by adding the right overloads.Peterman

© 2022 - 2024 — McMap. All rights reserved.