4

最近我一直在尝试使用这个Func<T>类,到目前为止我很喜欢它。然而,我注意到我越来越多地开始使用它而不是实际使用 的实例T,所以我想问一下;使用vs的开销是多少?Func<T>T我知道这是一个有点笼统的问题,T任何事情都可以,所以我想这个问题应该集中在传递函数而不是简单对象的实例的开销上?

为了论证,让我们假设以下内容。

我们的模拟对象,T

public class Person
{
    private string _name = string.Empty;
    private int _age = 0;
    private bool _isMale = true;

    public Person(string name, int age, bool isMale)
    {
        this.Name = name;
        this.Age = age;
        this.IsMale = isMale;
    }

    public string Name
    {
        get { return this._name; }
        set { this._name = value; }
    }

    public int Age
    {
        get { return this._age; }
        set { this._age = value; }
    }

    public bool IsMale
    {
        get { return this._isMale; }
        set { this._isMale = value; }
    }
}

现在,假设我们有一个漂亮的扩展方法 on IDictionary,它通过键选择值默认值。伪代码可以描述如下:
KeyValuePair 集合中是否找到键 是,返回值 否,返回默认值

选项1.我们的扩展方法使用一个实例T

public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary source, TKey key, TValue @default)
{
    if (source.ContainsKey(key))
    {
        return source[key];
    }
    return @default;
}

// usage
var myValue = myDictionary.GetValueOrDefault("Richard", new Person());

选项 2. 我们的扩展方法使用Func<T>... 嗯,漂亮!

public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary source, TKey key, Func<TValue> defaultSelector)
{
    if (source.ContainsKey(key))
    {
        return source[key];
    }
    return defaultSelector();
}

// usage
var myValue = myDictionary.GetValueOrDefault("Richard", () => new Person("Richard", 25, true));

比较

比较上述选项,很明显两者都有潜在的好处。选项 1 更容易阅读,但是我目前喜欢使用Func<T>,因此对我来说选项 2 似乎很理想。我想我认为它是一个惰性实例化的参数,它只在需要时执行,因此可以节省效率,但我说的对吗?

4

4 回答 4

6

这是我用于基准测试的代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace ConsoleApplication3
{
    using System.Collections;
    using System.Diagnostics;
    using System.Globalization;
    using System.Numerics;
    using System.Xml.Linq;

    public class Program
    {

        public class Person
        {
            private string _name = string.Empty;

            private int _age = 0;

            private bool _isMale = true;

            public Person(string name, int age, bool isMale)
            {
                this.Name = name;
                this.Age = age;
                this.IsMale = isMale;
            }

            public string Name
            {
                get
                {
                    return this._name;
                }
                set
                {
                    this._name = value;
                }
            }

            public int Age
            {
                get
                {
                    return this._age;
                }
                set
                {
                    this._age = value;
                }
            }

            public bool IsMale
            {
                get
                {
                    return this._isMale;
                }
                set
                {
                    this._isMale = value;
                }
            }
        }

        private static void Main(string[] args)
        {
            var myDictionary = new Dictionary<string, Person>();
            myDictionary.Add("notRichard", new Program.Person("Richard1", 26, true));
            myDictionary.Add("notRichard1", new Program.Person("Richard2", 27, true));
            myDictionary.Add("notRichard2", new Program.Person("Richard3", 28, true));
            myDictionary.Add("notRichard3", new Program.Person("Richard4", 29, true));
            // usage
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for(int i = 0; i < 100000000; i++)
            {
                var myValue = myDictionary.GetValueOrDefault("Richard", new Program.Person("Richard", 25, true));
            }
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
            sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 100000000; i++)
            {
                var myValue = myDictionary.GetValueOrDefault("Richard", ()=> new Program.Person("Richard", 25, true));
            }
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
            Console.ReadKey();
        }
    }
    public static class Ex
    {
        public static TValue GetValueOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> source, TKey key, TValue @default)
        {
            if (source.ContainsKey(key))
            {
                return source[key];
            }
            return @default;
        }
        public static TValue GetValueOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> source, TKey key, Func<TValue> defaultSelector)
        {
            if (source.ContainsKey(key))
            {
                return source[key];
            }
            return defaultSelector();
        }


    }
}

调用每个扩展方法100000000次(没有找到条目,因此导致 Func 每次都被执行)给出以下结果:

T- 10352 毫秒

Func<T>- 12268 毫秒

调用每个扩展方法100000000次(并找到一个条目,因此根本不调用 Func)给出以下结果:

T- 15578 毫秒

Func<T>- 11072 毫秒

因此,哪个执行得更快取决于您节省了多少实例化以及每个实例化的成本。

通过重用默认的 person 实例来稍微优化代码,为 6809 毫秒T和 7452毫秒Func<T>

            Stopwatch sw = new Stopwatch();
            var defaultPerson = new Program.Person("Richard", 25, true);
            sw.Start();
            for(int i = 0; i < 100000000; i++)
            {
                var myValue = myDictionary.GetValueOrDefault("Richard", defaultPerson);
            }
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
            sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 100000000; i++)
            {
                var myValue = myDictionary.GetValueOrDefault("Richard", () => defaultPerson);
            }

因此,理论上(如果您将实例化排除在等式之外),在调用堆栈中保存一个跃点会给您一些性能提升,但在实践中,这种提升可以忽略不计。

于 2012-05-23T22:57:37.347 回答
1

传递对函数的引用和传递对对象的引用我希望非常相似。如果您正在执行一百万次这样的调用,那么两者都可能会通过保存引用并每次重用相同的值而受益。但请注意,在第一种情况下,每次都可以使默认对象相同:

Person defaultPerson = new Person();
var myValue = myDictionary.GetValueOrDefault("Richard", defaultPerson);

Person但在第二种情况下,每次返回默认值时,您都会实例化一个全新的:

Func<Person> defaultPersonFunc = () => new Person("Richard", 25, true);
var myValue = myDictionary.GetValueOrDefault("Richard", defaultPersonFunc);

您可以通过更改为:

Person defaultPerson = new Person("Richard", 25, true);
Func<Person> defaultPersonFunc = () => defaultPerson;
var myValue = myDictionary.GetValueOrDefault("Richard", defaultPersonFunc);

但是我看不出你会通过使用Func<Person>

于 2012-05-23T23:00:34.290 回答
0

第二种方法效率低,因为它要求编译器在每次调用函数时生成一个闭包(委托和对象实例的组合),而不管是否实际需要闭包。除非Person创建成本很高,否则无条件生成闭包将比无条件生成Person.

另一种方法是让 lambda 成为一个静态方法,它接受其参数作为 ref 结构。我希望 C# 能为这种方法提供一些语言支持,因为它可以更有效地完成许多闭包可以做的事情。代码看起来像:

公共委托 TResult TFuncByRef<TParam,TResult>(ref TParam);
公共静态 TValue GetValueOrDefault<TKey, TValue, TParam>
  (此 IDictionary 源,TKey 键,
   FuncByRef<TParam, TValue> defaultSelector,
   参考 TParam 参数)
{
    参考 TValue 结果 = 默认(TValue);
    if (!source.TryGetValue(key, ref Result))
        结果 = 默认选择器(参考参数);
    返回结果;
}

struct CreatePersonParams {公共字符串名称;公共int年龄;公共布尔 IsMale};
静态人 CreatePersonByName(参考 CreatePersonParams 参数)
{
  return new Person(param.Name, param.Age, param.IsMale);
}
...然后使用它...
{
  ...
  CreatePersonParams newPersonParams;
  newPersonParams.Name = "艾米丽";
  newPersonParams.Age = 23;
  newPersonParams.IsMale = False;
  ...
  不管 = myDict.GetValueOrDefault(keyValue, CreatePersonByName, ref newPersonParams);
  ...
}

请注意,将结构实例创建为局部变量比创建新的类实例便宜(本质上与为每个结构字段创建局部变量相同)。另请注意,由于 CreatePersonByName 是一个静态方法,因此系统只需要在程序的生命周期内创建一个委托。进一步注意,无论结构有多大,通过 ref 传递结构都很便宜,并且访问通过 ref 传递的结构的字段与访问类的字段一样有效。

使用这种方法,相同的GetValueOrDefault方法可以处理需要任何参数组合的例程,而无需创建任何闭包。不幸的是,由于 C# 不提供任何语言支持这种类型的代码转换(这实际上可能比将 lambda 转换为闭包更简单),因此有必要显式定义可能想要使用的所有结构类型。可以定义一系列TupleStruct<T1,T2,T3>等结构并使用它们,但这样的东西仍然有点难看。

于 2012-05-24T15:38:43.307 回答
0

代表很棒,但我不确定这是您从使用它们中受益的情况。

选项 1 效率低下,因为您无条件地创建了一个新的对象实例:

var myValue = myDictionary.GetValueOrDefault("Richard", new Person());

总是会创建一个新的 Person 。

选项 2 效率低,因为您创建了一个新Func实例。你的语法是这个的简写:

var myValue = myDictionary.GetValueOrDefault("Richard", new Func<Person>(()=> {
    Person("Richard", 25, true));
});

编译器可能会比另一个优化更多,但最终,这是使用错误工具解决问题的情况。解决方案是使用内置TryGetValue方法,仅在需要时才实际运行默认代码:

Person myValue;
if (!myDictionary.TryGetValue("Richard", out myValue)) {
    myValue = new Person("Richard",25,true);
}

当目标不在字典中时,从我得到的另一个答案运行测试:

  • 7309 毫秒(选择 1)
  • 8705 毫秒(选项 2)
  • 5972 毫秒(TryGetValue)

当目标在字典中时(例如,它永远不需要使用默认值):

  • 10026 毫秒(选项 2)
  • 7712 毫秒(选项 2)
  • 3491 毫秒(TryGetValue)

无论如何,它显然要快得多。这是有道理的——因为每次对选项 1 或 2 进行测试时,无论结果如何,您都必须创建一个对象。在这种Func情况下,您必须在需要默认值时创建两个对象,在不需要时创建一个对象,因此,当总是需要默认值时,它的表现最差。

另一方面,仅使用TryGetValue并且仅有条件地执行默认代码,您只需要在需要默认代码时创建一个对象(并且只是您真正需要的对象)。

有时老式的方式是最好的:)

FWIW - 我认为像这样的方法GetValueOrDefault肯定很有用,但当你必须明确定义默认值时可能不是。也就是说,就代码风格而言,我没有看到使用委托的巨大好处,当然也没有性能上的好处。但是,如果您不需要实际定义默认人员的内容,那么为什么不直接创建一个这样的扩展方法:

public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary source, 
    TKey key) where TValue: new()
{
    TValue value;
    if (!source.TryGetValue(key, out value)) {
        value = new TValue();
    }
    return value;
}
于 2012-05-24T12:27:47.217 回答