【问题标题】:Is it possible to create a contiguous multidimensional array at runtime in D?是否可以在运行时在 D 中创建一个连续的多维数组?
【发布时间】:2022-01-01 05:34:02
【问题描述】:

我想在运行时创建一个矩形多维数组,以便整个数组存储在一个连续的内存块中。数组在初始创建后不需要调整大小。实际上,我想在运行时创建一个静态数组,但我会接受满足所述条件的方法,即使该数组在技术上属于不同类型。

更正式地说,我想取两个ulongs nrnc,并在运行时创建一个数组arr,这样arr[r][c] 就等于*(arr.ptr + r * nc + c),两者都在什么方面它评估,以及它这样做的效率。 (*(arr.ptr + c * nr + r) 也是可以接受的,虽然我不认为 D 会使用列优先顺序。)

有没有办法在 D 中做到这一点?

我得到的最接近的是:

import std.stdio, core.memory;

void main() {
    ulong nr = 3, nc = 4;
    auto p = cast(int*)GC.malloc(nr * nc * int.sizeof);
    auto p1 = cast(int[3][4])p[0..12];
}

但如果我将 cast(int[3][4]) 更改为 cast(int[nr][nc]),则编译失败。


答案比较

为了比较提供的不同答案,我在 10,000,000 x 20 的双精度数组上运行了以下函数(根据需要调整了索引表达式)。

auto busywork(T)(T p) {
    foreach (r; 0..nr) {
        foreach (c; 0..nc) {
            p[r][c] = r + log(1.0 + c);
        }
    }
    double result = 1.0;
    foreach (t; 0..1) {
        foreach (r; 0..nr) {
            foreach (c; 0..nc) {
                result += log(1.0 + pow(p[r][c], 3));
            }
        }
    }
    return result;
}
  • 基线是使用p[r * nc + c]手动索引的一维数组。
  • baseline_dynamic 是一个二维动态数组
  • chunks_array 是 Bearded Beaver 的方法
  • chunks 是 Bearded Beaver 不带 .array 的方法
  • array2D 是 Jack Applegame 的 (2D) 方法,其中边界检查已从 opIndex 中移除。

运行时

该表报告了五次运行的中位运行时间(以毫秒为单位)。

dmd -O gdc -O3 gdc -Os ldc -O3
baseline 13592 +/- 1758 20978 +/- 434 15452 +/- 1178 20778 +/- 1017
baseline_dynamic 13481 +/- 339 20782 +/- 501 13476 +/- 209 20632 +/- 156
chunks 15800 +/- 144 21014 +/- 176 13676 +/- 118 21203 +/- 212
chunksarray 12903 +/- 649 19648 +/- 1080 12895 +/- 519 19770 +/- 816
array2d 12606 +/- 606 19844 +/- 1042 12640 +/- 540 19724 +/- 953

内存使用

  • chunks 和 array2d 的内存开销均为零。
  • chunksarray 对所有编译器都有 9% 的开销。
  • baseline_dynamic 在 dmd 下有 18% 的开销,在 gcc 和 ldc 下有 41% 开销。

自然,相对开销因数组尺寸和类型而异。

结论

-O3-Os 之间的巨大差异,以及 array2ddmd -O 下的事实在某些运行中比基线执行更好(尽管检查生成的程序集(一个较短的程序)显示opIndex 没有被 dmd 内联)表明缓存未命中的数量是主要因素,至少当阵列占用了相当比例的系统 RAM 时。 (设置 -boundscheck=off 可使 dmd -O 版本的基线与 array2d 性能保持一致,所以这显然是 array2d 有时更好的原因。)

chunksarray 的优点是代码更短并且使用与动态数组相同的语法,并且在 nc 和/或 T.sizeof 足够大以至于相对内存开销变得不那么重要的情况下可能会变得更可取,但是 array2d在一般情况下似乎是最佳选择。

【问题讨论】:

  • 感谢对比表,干得好。我想知道为什么 ldc 这么慢,它被认为非常快
  • @BeardedBeaver 我怀疑不是 ldc 表现不佳,而是-O3gcc -O3 的表现与ldc2 -O3 相似,但gcc -Os 表现相当不错。我在 ldc 手册中找不到优化代码大小的标志,因此表中没有。

标签: arrays d memory-layout


【解决方案1】:

D 中的静态和动态数组在内存中的布局不同。

动态数组是引用类型,静态数组是值类型。

所以,没有办法将一块内存转换成多维动态数组。

相反,您必须使用重载的索引运算符创建自己的类型。

基本示例:

import std.stdio : writeln;

void main() {
    auto arr = Array2D!int(3, 4);
    arr[2, 3] = 23;
    // arr[3, 3] = 33; // range violation
    writeln(arr[2, 3]);
    writeln(arr.data[0..12]);
}

struct Array2D(T) {
    import core.memory : GC;
    import std.exception : enforce;
    import core.exception : RangeError;
    
    size_t nr, nc;
    T* data;
    this(size_t nr, size_t nc) {
        this.nr = nr;
        this.nc = nc;
        auto ds = nr * nc;
        data = cast(T*) GC.malloc(T.sizeof * ds);
        data[0..ds] = 0;
    }
    
    ref T opIndex(size_t r, size_t c) {
        enforce!RangeError(r >= 0 && r < nr);
        enforce!RangeError(c >= 0 && c < nc);
        return data[c * nr + r];
    }
}

基本通用示例:

import std.stdio : writeln;

void main() {
    auto arr = makeArrayMD!int(2, 3, 4);
    arr[1, 2, 3] = 123;
    // arr[2, 2, 3] = 223; // range violation
    writeln(arr[1, 2, 3]);
    writeln(arr.data[0..24]);
}

struct ArrayMD(T, size_t dims) {
    import core.memory : GC;
    import std.algorithm : fold;
    import std.exception : enforce;
    import core.exception : RangeError;
    
    size_t[dims] sizes;
    T* data;
    this(Sizes...)(Sizes szs) if(Sizes.length == dims) {
        sizes = [szs];
        auto ds = sizes.fold!"a * b";
        data = cast(T*) GC.malloc(T.sizeof * ds);
        data[0..ds] = 0;
    }
    
    ref T opIndex(Sizes...)(Sizes szs) {
        size_t idx = 0;
        size_t rs = 1;
        static foreach(i, s; szs) {
            enforce!RangeError(s >= 0 && s < sizes[i]);
            static if(i > 0) rs *= sizes[i - 1];
            idx += s * rs;
        }
        return data[idx];
    }
}

auto makeArrayMD(T, DS...)(DS dims) {
    return ArrayMD!(T, DS.length)(dims);
}

【讨论】:

  • 不幸的是,dmd 会为opIndex 生成实际的call 指令,即使使用pragma(inline, true) 也是如此(尽管如果我在全局范围内打开-inline,它会内联它)。
  • 如果您想要运行时优化,请使用 gdc 或 ldc; dmd 专注于快速编译时间而不是快速生成代码。 (其生成的代码通常足够好,但从未进行过深度优化)
  • @AdamD.Ruppe 至少对于我测试这些答案所针对的值,事实证明并非如此。 dmd -Ogdc -Os 的表现大致相同,-O3gdcldc2 都差得多。
  • 是的,有时也有例外。不过,我有点惊讶,通常 ldc+gdc 在这种代码上做得更好。哦,好吧,不管用什么
【解决方案2】:

我使用chunks 创建了一个二维数组,它在内存中一块存储

auto p = new int[](nr * nc);
auto p1 = p.chunks(nc).array;

// now you can set and get values as in usual 2D array
p1[i][j] = 42;

更多信息在这里:https://dlang.org/library/std/range/chunks.html#1

【讨论】:

  • 创建包装器的好方法!而不是p[i][j],应该有p1[i][j]
  • 你也不需要arrayauto p1 = p.chunks(nc); 就够了。
  • 它可以在没有array 的情况下工作,但根据我的测试,它明显比array 慢,这就是我添加它的原因。
  • 使用array,将创建第二个数组。但它确实更快,然后是 struct wrapper。
  • 我接受了杰克的回答,因为它提供了更好的性能,但为了简洁起见 +1。对于我愿意接受.array 版本的内存开销或没有.array 的版本的性能影响的情况,值得在我的工具箱中使用一个简短版本。
猜你喜欢
  • 1970-01-01
  • 2011-11-23
  • 1970-01-01
  • 2012-12-14
  • 1970-01-01
  • 2013-07-08
  • 1970-01-01
  • 2016-03-18
  • 1970-01-01
相关资源
最近更新 更多