如何防止在级联运算符中创建中间对象?

时间:2020-03-06 14:32:23  来源:igfitidea点击:

我在应用程序中使用自定义的Matrix类,并且经常添加多个矩阵:

Matrix result = a + b + c + d; // a, b, c and d are also Matrices

但是,这为每个加法操作创建了一个中间矩阵。由于这是简单的加法运算,因此可以通过一次性添加所有4个矩阵的元素来避免中间对象并创建结果。我该怎么做?

注意:我知道我可以定义多个函数,例如Add3Matrices(a,b,c)Add4Matrices(a,b,c,d)等,但是我想保持result = a + b的优美性+ c + d

解决方案

使用运算符是不可能的。

在C ++中,可以使用模板元程序,也可以在此处使用模板来做到这一点。但是,模板编程并非易事。我不知道C#是否提供类似的技术,很可能没有。

C ++中的这项技术完全可以满足需求。缺点是,如果某些事情不太正确,则编译器错误消息会运行到几页,并且几乎无法破译。

如果没有这种技术,我怀疑我们只能使用Add3Matrices之类的功能。

但是对于Cthis链接可能正是我们所需要的:Calth中的高效矩阵编程,它的工作方式似乎与C ++模板表达式略有不同。

至少可以避免痛苦的东西

Matrix Add3Matrices(a,b,c) //and so on

将是

Matrix AddMatrices(Matrix[] matrices)

我们无法避免创建中间对象。

但是,我们可以按照此处所述使用表达模板,以将其最小化,并对模板进行懒惰的评估。

在最简单的级别上,表达式模板可以是一个对象,该对象存储对多个矩阵的引用,并在分配时调用诸如Add3Matrices()之类的适当函数。在最高级的级别上,表达式模板将执行一些操作,例如根据请求以惰性方式计算最少的信息量。

我们可以通过使用惰性评估将自己限制在一个小的中间中间体上。就像是

public class LazyMatrix
{
    public static implicit operator Matrix(LazyMatrix l)
    {
        Matrix m = new Matrix();
        foreach (Matrix x in l.Pending)
        {
            for (int i = 0; i < 2; ++i)
                for (int j = 0; j < 2; ++j)
                    m.Contents[i, j] += x.Contents[i, j];
        }

        return m;
    }

    public List<Matrix> Pending = new List<Matrix>();
}

public class Matrix
{
    public int[,] Contents = { { 0, 0 }, { 0, 0 } };

    public static LazyMatrix operator+(Matrix a, Matrix b)
    {
        LazyMatrix l = new LazyMatrix();
        l.Pending.Add(a);
        l.Pending.Add(b);
        return l;
    }

    public static LazyMatrix operator+(Matrix a, LazyMatrix b)
    {
        b.Pending.Add(a);
        return b;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Matrix a = new Matrix();
        Matrix b = new Matrix();
        Matrix c = new Matrix();
        Matrix d = new Matrix();

        a.Contents[0, 0] = 1;
        b.Contents[1, 0] = 4;
        c.Contents[0, 1] = 9;
        d.Contents[1, 1] = 16;

        Matrix m = a + b + c + d;

        for (int i = 0; i < 2; ++i)
        {
            for (int j = 0; j < 2; ++j)
            {
                System.Console.Write(m.Contents[i, j]);
                System.Console.Write("  ");
            }
            System.Console.WriteLine();
        }

        System.Console.ReadLine();
    }
}

这不是最干净的解决方案,但是如果我们知道评估顺序,则可以执行以下操作:

result = MatrixAdditionCollector() << a + b + c + d

(或者同一个名称不同的东西)。然后,MatrixCollector将+实现为+ =,即从未定义大小的0矩阵开始,在对第一个+求值后取一个大小并将所有内容加在一起(或者复制第一个矩阵)。这样可以将中间对象的数量减少到1(或者如果我们以良好的方式实现分配,则可以减少为0,因为MatrixCollector可能会立即包含结果)。
我不完全确定这是否像地狱般丑陋,还是可能做的更好的骇客之一。一个明显的优势是发生的事情很明显。

我的第一个解决方案是遵循以下原则(如果可能,添加Matrix类):

static Matrix AddMatrices(Matrix[] lMatrices) // or List<Matrix> lMatrices
{
    // Check consistency of matrices

    Matrix m = new Matrix(n, p);

    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            foreach (Maxtrix mat in lMatrices)
                m[i, j] += mat[i, j];

    return m;
}

我在Matrix类中使用了它,因为在矩阵的实现发生变化的情况下,我们可以依赖对函数有用的私有方法和属性(例如,非空节点的链接列表而不是大的double数组) )。

当然,我们会失去result = a + b + c + d的优雅。但是,我们会得到类似"结果= Matrix.AddMatrices(new Matrix [] {a,b,c,d});''的内容。

我认为我们可以将所需的就地添加行为明确化:

Matrix result = a;
result += b;
result += c;
result += d;

但是正如Doug在这篇文章的评论中所指出的那样,编译器将这段代码视为与我编写过的代码相同:

Matrix result = a;
result = result + b;
result = result + c;
result = result + d;

因此仍会创建临时对象。

我只是删除此答案,但是似乎其他人可能有相同的误解,因此请考虑一下这是一个反例。

可能我建议我们使用一个行为很像StringBuilder的MatrixAdder。我们将矩阵添加到MatrixAdder中,然后调用ToMatrix()方法,该方法将在惰性实现中为我们进行添加。这将为我们提供所需的结果,可以扩展到任何形式的LazyEvaluation,但是也不会引入任何可能使其他代码维护者感到困惑的聪明实现。

Bjarne Stroustrup撰写了一篇简短的论文,名为《 C ++中的抽象,库和效率》,他提到了用于实现所需内容的技术。特别是,他提到了Blitz ++库,它是用于科学计算的库,它对矩阵也具有高效的运算能力,还有一些其他有趣的库。另外,我建议我们阅读artima.com上与Bjarne Stroustrup的对话。

有几种方法可以实现惰性评估来实现这一目标。但请记住,重要的是,并非总是编译器将能够获得所有代码中的最佳代码。

我已经做出了在GCC中效果很好的实现,甚至超过了传统的For不可读代码的性能,因为它们使编译器发现数据段之间没有别名(难以理解的数组不存在)。但是其中一些在MSVC上是完全失败的,反之亦然。不幸的是,这些代码太长了,无法在此处发布(不要认为数千行代码适合此处)。

Blitz ++是一个非常复杂的库,具有丰富的嵌入式知识,是用于科学计算的库。