【问题标题】:Calculate the sum of values in an array using renderscript使用渲染脚本计算数组中值的总和
【发布时间】:2015-10-15 00:35:48
【问题描述】:

您好,我是一个新手,正在尝试在 Renderscript 中编写代码。我想知道如何使用渲染脚本执行数组中的元素总和。有没有办法可以将输出传递回脚本以进行顺序添加?我的问题陈述是: 向量和

说明:计算数组中值的总和。

输入:整数数组

输出:整数

任何帮助将不胜感激!

【问题讨论】:

    标签: arrays renderscript


    【解决方案1】:

    恐怕这比看起来要复杂一些,但我会尽我所能在此解释您可以采取的实现方法。

    您所要求的更为人所知的是并行缩减算法,它可以像您的情况一样实现数组总和,或任何其他交换+关联运算符,当在数组上迭代应用时,它将“减少”它到一个号码。其他示例是查找大型数组的最大值或最小值。在 CUDA 和 OpenCL 中,这种计算有一个众所周知的模式,它能够最好地利用并行线程,例如,如果你在谷歌上搜索“CUDA reduction”,你会得到关于这个算法的大量有用信息。

    实现的方法是反复将数组减半,一遍又一遍,直到最终得到一个元素。每次减少它时,每个新元素都是前两个元素的总和。这是一张更能描述这个算法的图片:

    例如,您从一个 16 元素数组开始。运行一次算法,最终得到一个 8 元素数组——这 8 个元素中的每一个都是原始数组中两个数字的总和。

    您再次运行它,最终得到 4 个元素 - 其中每个元素都是上一步中两个数字的总和。等等……

    你一直这样做,直到你最终只得到一个数字——你的总和。

    在 RenderScript 中实现这一点的一种低效方式是:

    Java:

    int[] ints; // Your data is held here.
    
    Allocation data = Allocation.createSized(rs, Element.I32(rs), ints.length, Allocation.USAGE_SCRIPT);
    data.copy1DRangeFrom(0, ints.length, ints);
    
    ScriptC_Reduce script = new ScriptC_Reduce(rs);
    script.bind_data(data);
    
    for (int stride = ints.length / 2; stride > 0; stride /= 2) {
        script.set_stride(stride);
        script.forEach_root(input, output);
    }
    
    data.copyTo(ints);
    int totalsum = ints[0];
    

    渲染脚本:

    #pragma version(1)
    #pragma rs java_package_name(...[your package here]...)
    
    int stride;
    int * data;
    
    void root(const int32_t *v_in, int32_t *v_out, uint32_t x) {
        if (x < stride) data[x] += data[x + stride];
    }
    

    如果您以前使用过 RS,您可能会注意到一些奇怪的事情:

    1. 注意RS内核中的“v_in”和“v_out”根本没有用到,因为它们仅限于读写当前线程索引对应的数据元素,而reduce算法需要在其他职位。因此,有一个 int 数组指针“数据”是从 Java 绑定的同名分配,这就是内核直接工作的内容。
    2. 在 Java 中从循环中多次调用内核,而不是在内核中执行该循环。这是因为在每次迭代中,前一步中的所有数据都必须已经在其预期位置准备好,否则,“data[x + stride]”将不同步。在 RS 中,内核调用锁定,这意味着在内核完成处理整个数据之前不会执行任何其他操作。这类似于 __syncthreads() 在 CUDA 内核中执行的操作,如果您熟悉的话。

    但是,我在上面提到过,这是一种低效的实现方式。但它应该为您指明正确的方向。为了提高效率,您可能需要将数据拆分为较小的块以分别计算,因为如此处给出的,此算法将在每个迭代步骤中运行 ints.length 个线程数,并且在非常大的数组上运行,这将导致lot 个步骤,每一步都有 lot 个空闲线程。

    此外,这假设您的数组的长度恰好是 2 的幂,因此多次减半将导致恰好一个元素。对于其他大小的数组,您可能需要对数组进行 0 填充。同样,在处理非常大的数组时,0-padding 将需要大量浪费的内存。

    因此,要解决这些问题,您可能需要将数组拆分为多个块,例如,每个块包含 64 个元素。因此,如果您没有确切的数组长度,则将“最后一个”块填充到 64 将不需要那么多内存。此外,您将需要更少的迭代步骤(和更少的空闲线程)来减少 64 个元素。当然,64是我刚刚编出来的一个神奇的数字。尝试其他 2 的幂来查看它们的结果,如果使用其他块大小(例如 16 或 32),您可能会看到更好的结果。我怀疑性能与块大小将非常依赖于硬件。

    编辑: 这假设 RenderScript 可以为其运行所在的设备使用 GPU 驱动程序,以便它实际上可以启动更多的并行线程。否则,像这样仅在 CPU 上执行内核可能会比处理线性数组还要慢。

    【讨论】:

    • 这种方法应该适用于非常大的数组。对于较小的数组,有成千上万个元素,一个简单的循环可能更快。
    • 看完这篇文章后我有点困惑。您将方法“root”称为内核,但从 Android 文档中指出内核具有标识符 __attribute__((kernel)) 因此这是一个可调用的函数。因此,它是一个单线程函数。我在这里有什么遗漏吗?
    • root(...) 是 RenderScript 文件中的一个特殊保留方法名称,它始终充当该类的主内核。尽管您可以通过使用您提到的 kernel 属性指定它们来将 additional 内核添加到同一类,但根内核始终是“默认”。然后,从 java 端,它被称为script.forEach_root(...)。
    • 实际上对于较小的数组,简单的循环已经更快了。它们基本上已经是原生的、高度优化的和附加的。任何有开销的东西都会变慢。我尝试在一台设备上计算的渲染脚本选项与 400 万个项目的简单循环相当。使用一些额外的处理器需要很长时间才能赶上只使用一个处理器并且没有开销,而只是添加就行了。
    【解决方案2】:

    不要。除非你有比 1 加法更高级的东西。不。在该数组中至少有 400 万个整数之前,代码不会更快。

    RenderScript: Entries:1 Total: 3 Time: 0.067ms
    Simple Loop : Entries:1 Total: 3 Time: 0.001ms
    RenderScript: Entries:2 Total: 97 Time: 0.614ms
    Simple Loop : Entries:2 Total: 97 Time: 0.001ms
    RenderScript: Entries:4 Total: 227 Time: 0.28ms
    Simple Loop : Entries:4 Total: 227 Time: 0.002ms
    RenderScript: Entries:8 Total: 320 Time: 0.445ms
    Simple Loop : Entries:8 Total: 320 Time: 0.002ms
    RenderScript: Entries:16 Total: 700 Time: 0.486ms
    Simple Loop : Entries:16 Total: 700 Time: 0.002ms
    RenderScript: Entries:32 Total: 1807 Time: 0.595ms
    Simple Loop : Entries:32 Total: 1807 Time: 0.002ms
    RenderScript: Entries:64 Total: 3218 Time: 0.624ms
    Simple Loop : Entries:64 Total: 3218 Time: 0.002ms
    RenderScript: Entries:128 Total: 6230 Time: 0.737ms
    Simple Loop : Entries:128 Total: 6230 Time: 0.003ms
    RenderScript: Entries:256 Total: 12968 Time: 0.769ms
    Simple Loop : Entries:256 Total: 12968 Time: 0.005ms
    RenderScript: Entries:512 Total: 26253 Time: 0.895ms
    Simple Loop : Entries:512 Total: 26253 Time: 0.01ms
    RenderScript: Entries:1024 Total: 52345 Time: 0.987001ms
    Simple Loop : Entries:1024 Total: 52345 Time: 0.017ms
    RenderScript: Entries:2048 Total: 100223 Time: 1.715ms
    Simple Loop : Entries:2048 Total: 100223 Time: 0.034ms
    RenderScript: Entries:4096 Total: 200375 Time: 1.213ms
    Simple Loop : Entries:4096 Total: 200375 Time: 0.065ms
    RenderScript: Entries:8192 Total: 403713 Time: 1.196ms
    Simple Loop : Entries:8192 Total: 403713 Time: 0.163001ms
    RenderScript: Entries:16384 Total: 812411 Time: 1.929ms
    Simple Loop : Entries:16384 Total: 812411 Time: 0.41ms
    RenderScript: Entries:32768 Total: 1620542 Time: 1.822ms
    Simple Loop : Entries:32768 Total: 1620542 Time: 0.617ms
    RenderScript: Entries:65536 Total: 3250733 Time: 5.955ms
    Simple Loop : Entries:65536 Total: 3250733 Time: 1.384ms
    RenderScript: Entries:131072 Total: 6478866 Time: 2.622ms
    Simple Loop : Entries:131072 Total: 6478866 Time: 2.008ms
    RenderScript: Entries:262144 Total: 12980832 Time: 3.979999ms
    Simple Loop : Entries:262144 Total: 12980832 Time: 4.377001ms
    RenderScript: Entries:524288 Total: 25956676 Time: 10.163ms
    Simple Loop : Entries:524288 Total: 25956676 Time: 8.326ms
    RenderScript: Entries:1048576 Total: 51897168 Time: 12.723001ms
    Simple Loop : Entries:1048576 Total: 51897168 Time: 15.871999ms
    RenderScript: Entries:2097152 Total: 103867356 Time: 32.229001ms
    Simple Loop : Entries:2097152 Total: 103867356 Time: 31.367ms
    RenderScript: Entries:4194304 Total: 207646704 Time: 61.628999ms
    Simple Loop : Entries:4194304 Total: 207646704 Time: 63.378ms
    RenderScript: Entries:8388608 Total: 415058480 Time: 103.734999ms
    Simple Loop : Entries:8388608 Total: 415058480 Time: 140.088ms
    

    这是为了渲染脚本而削减一切。就像假设所有分配都将在主循环之外完成,并且它不必将数据数组复制回来(我只是调用 rs.finish() 来确保渲染脚本完成)。

    #pragma version(1)
    #pragma rs java_package_name(com.photoembroidery.tat.olsennoise)
    
    int stride;
    int * data;
    
    void root(const int32_t *v_in, int32_t *v_out, uint32_t x) {
        data[x] += data[x + stride];
    }
    

    注意启动选项。您进行第一次缩减以将数组缩减到正确的因子 2。因此,您可以在此之前取大小和因子 2 之间的余数,并处理这些条目,以便它们与其余的因子一起减少。然后处理二的因数。

    //int[] array = //array of your data//;
    
            ScriptC_reduce script = new ScriptC_reduce(mRS);
            Allocation data = Allocation.createSized(mRS, Element.I32(mRS), array.length, Allocation.USAGE_SCRIPT);
            data.copy1DRangeFrom(0, array.length, array);
            script.bind_data(data);
    
            int smallest2ExpBiggerThanLength = 1;
            for (int length = arraysize; length != 0; length >>= 1,smallest2ExpBiggerThanLength <<= 1);
    
            int end = smallest2ExpBiggerThanLength / 2;
            int start = smallest2ExpBiggerThanLength - arraysize;
            if (start == end) {
                start = 0;
                end = end/2;
            }
    
            while (end > 0) {
                launchOptions.setX(start, end);
                script.set_stride(end - start);
                script.forEach_root(data, data, launchOptions);
                script.forEach_root(data,data);
                end = end >> 1;
                start = 0;
            }
            data.copyTo(array);
            int total = array[0];
    

    另一个答案中最大的低效率是启动选项。你最好从一开始就限制范围,而不是检查范围的有效性。你失去了 4 倍的速度。一个简单的循环将更快、更通用,而不会调用渲染脚本错误。 -- 你需要做一些比 1 add 更难的事情才能使这个值。

    【讨论】:

      【解决方案3】:

      我们实际上正在努力支持“reduce”作为下一个 Android 版本的新内核类型。这将允许您在分配的单元格上运行关联操作(如加法),并返回一个简化的结果。 AOSP 中已经存在这方面的代码,但我们正在努力使其更加灵活/通用。当前的表单已经允许您指定可应用于所有单元的 2 输入 -> 1 输出内核。

      同时,您可以通过在可调用对象中按顺序运行并使用 rsGetElementAt_*() 遍历单元格来近似化约内核。它会比 Java 快得多,在这种情况下,您需要不断地为不必要的边界检查(以及其他开销)付费。

      【讨论】:

        猜你喜欢
        • 2013-05-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-08-05
        • 1970-01-01
        • 1970-01-01
        • 2014-01-02
        相关资源
        最近更新 更多