oneAPI GPU 优化指南 - 指针别名和 Restrict 指令

本章节翻译by chenshusmail@163.com 原文:Pointer Aliasing and the Restrict Directive (intel.com)

Kernel 通常对使用指针作为入参的元素数组进行操作。当编译器无法确定这些指针是否存在别名时, 它会保守地认为它们存在别名,在这种情况下,它不会对这些指针进行重排操作。 请看以下向量加法示例,其中每次循环迭代都有两次 load 和一次 store。

size_t VectorAdd(sycl::queue &q, const IntArray &a, const IntArray &b,
                 IntArray &sum, int iter) {
  sycl::range num_items{a.size()};

  sycl::buffer a_buf(a);
  sycl::buffer b_buf(b);
  sycl::buffer sum_buf(sum.data(), num_items);

  auto start = std::chrono::steady_clock::now();
  for (int i = 0; i < iter; i++) {
    auto e = q.submit([&](auto &h) {
      // Input accessors
      sycl::accessor a_acc(a_buf, h, sycl::read_only);
      sycl::accessor b_acc(b_buf, h, sycl::read_only);
      // Output accessor
      sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);

      h.parallel_for(num_items,
                     [=](auto i) { sum_acc[i] = a_acc[i] + b_acc[i]; });
    });
  }
  q.wait();
  auto end = std::chrono::steady_clock::now();
  std::cout << "Vector add completed on device - took " << (end - start).count()
            << " u-secs\n";
  return ((end - start).count());
} // end VectorAdd

在这个示例中,程序员将有关向量长度和 work-group 数量的所有选择权留给编译器。在大多数情况下, 编译器能很好地选择这些参数以获得良好性能。在某些情况下, 获得更高性能的代码的更好方法是显式地指定 work-group 的数量和大小,向编译器提供提示以获得良好性能。

下面的 kernel 被编写为每个 work-item 处理数组的多个元素,并显式选择 work-group 数量和大小。 第25行上的 intel::kernel_args_restrict 告诉编译器该 kernel 中的缓冲区访问器彼此不别名。 这将允许编译器提升 load 和 store 次序,从而为指令完成提供更多时间并获得更好的指令调度。 第27行上的 pragma 指示编译器循环展开系数为2。

size_t VectorAdd2(sycl::queue &q, const IntArray &a, const IntArray &b,
                  IntArray &sum, int iter) {
  sycl::range num_items{a.size()};

  sycl::buffer a_buf(a);
  sycl::buffer b_buf(b);
  sycl::buffer sum_buf(sum.data(), num_items);
  // size_t num_groups =
  // q.get_device().get_info<sycl::info::device::max_compute_units>(); size_t
  // wg_size =
  // q.get_device().get_info<sycl::info::device::max_work_group_size>();
  size_t num_groups = 1;
  size_t wg_size = 16;
  auto start = std::chrono::steady_clock::now();
  for (int i = 0; i < iter; i++) {
    q.submit([&](auto &h) {
      // Input accessors
      sycl::accessor a_acc(a_buf, h, sycl::read_only);
      sycl::accessor b_acc(b_buf, h, sycl::read_only);
      // Output accessor
      sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);

      h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
                     [=](sycl::nd_item<1> index) [[intel::reqd_sub_group_size(
                         16)]] [[intel::kernel_args_restrict]] {
                       size_t loc_id = index.get_local_id();
        // unroll with a directive
#pragma unroll(2)
                       for (size_t i = loc_id; i < mysize; i += wg_size) {
                         sum_acc[i] = a_acc[i] + b_acc[i];
                       }
                     });
    });
  }
  q.wait();
  auto end = std::chrono::steady_clock::now();
  std::cout << "Vector add2 completed on device - took "
            << (end - start).count() << " u-secs\n";
  return ((end - start).count());
} // end VectorAdd2

下面的 kernel 手动展开了循环而不是使用编译器指令(编译器可能会也可能不会根据其内部启发式成本模型遵守该指令)。 展开的优点是执行的指令更少,因为循环不必迭代那么多次,从而节省了比较和分支的指令。

size_t VectorAdd3(sycl::queue &q, const IntArray &a, const IntArray &b,
                  IntArray &sum, int iter) {
  sycl::range num_items{a.size()};

  sycl::buffer a_buf(a);
  sycl::buffer b_buf(b);
  sycl::buffer sum_buf(sum.data(), num_items);
  size_t num_groups = 1;
  size_t wg_size = 16;
  auto start = std::chrono::steady_clock::now();
  for (int i = 0; i < iter; i++) {
    q.submit([&](auto &h) {
      // Input accessors
      sycl::accessor a_acc(a_buf, h, sycl::read_only);
      sycl::accessor b_acc(b_buf, h, sycl::read_only);
      // Output accessor
      sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);

      h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
                     [=](sycl::nd_item<1> index)
                         [[intel::reqd_sub_group_size(16)]] {
                           // Manual unrolling
                           size_t loc_id = index.get_local_id();
                           for (size_t i = loc_id; i < mysize; i += 32) {
                             sum_acc[i] = a_acc[i] + b_acc[i];
                             sum_acc[i + 16] = a_acc[i + 16] + b_acc[i + 16];
                           }
                         });
    });
  }
  q.wait();
  auto end = std::chrono::steady_clock::now();
  std::cout << "Vector add3 completed on device - took "
            << (end - start).count() << " u-secs\n";
  return ((end - start).count());
} // end VectorAdd3

下面的 kernel 显示了如何对 load 和 store 进行重新排序,以便在对它们进行任何操作之前所有 load 都被发出。 通常,GPU 中的每个线程都可以有许多未完成的 load 。好的方式是在对它们进行任何操作之前发出 load 。 这样才可能在实际需要数据进行计算之前完成 load 操作。

size_t VectorAdd4(sycl::queue &q, const IntArray &a, const IntArray &b,
                  IntArray &sum, int iter) {
  sycl::range num_items{a.size()};

  sycl::buffer a_buf(a);
  sycl::buffer b_buf(b);
  sycl::buffer sum_buf(sum.data(), num_items);
  size_t num_groups = 1;
  size_t wg_size = 16;
  auto start = std::chrono::steady_clock::now();
  for (int i = 0; i < iter; i++) {
    q.submit([&](auto &h) {
      // Input accessors
      sycl::accessor a_acc(a_buf, h, sycl::read_only);
      sycl::accessor b_acc(b_buf, h, sycl::read_only);
      // Output accessor
      sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);

      h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
                     [=](sycl::nd_item<1> index)
                         [[intel::reqd_sub_group_size(16)]] {
                           // Manual unrolling
                           size_t loc_id = index.get_local_id();
                           for (size_t i = loc_id; i < mysize; i += 32) {
                             int t1 = a_acc[i];
                             int t2 = b_acc[i];
                             int t3 = a_acc[i + 16];
                             int t4 = b_acc[i + 16];
                             sum_acc[i] = t1 + t2;
                             sum_acc[i + 16] = t3 + t4;
                           }
                         });
    });
  }
  q.wait();
  auto end = std::chrono::steady_clock::now();
  std::cout << "Vector add4 completed on device - took "
            << (end - start).count() << " u-secs\n";
  return ((end - start).count());
} // end VectorAdd4

下面的 kernel 使用了 restrict 指令,它向编译器提供了一个提示,即循环内访问的向量之间没有别名, 编译器可以像在前面的示例中手动完成的那样,将 load 提升到 store 之前。

size_t VectorAdd5(sycl::queue &q, const IntArray &a, const IntArray &b,
                  IntArray &sum, int iter) {
  sycl::range num_items{a.size()};

  sycl::buffer a_buf(a);
  sycl::buffer b_buf(b);
  sycl::buffer sum_buf(sum.data(), num_items);
  size_t num_groups = 1;
  size_t wg_size = 16;
  auto start = std::chrono::steady_clock::now();
  for (int i = 0; i < iter; i++) {
    q.submit([&](auto &h) {
      // Input accessors
      sycl::accessor a_acc(a_buf, h, sycl::read_only);
      sycl::accessor b_acc(b_buf, h, sycl::read_only);
      // Output accessor
      sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);

      h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
                     [=](sycl::nd_item<1> index) [[intel::reqd_sub_group_size(
                         16)]] [[intel::kernel_args_restrict]] {
                       // compiler needs to hoist the loads
                       size_t loc_id = index.get_local_id();
                       for (size_t i = loc_id; i < mysize; i += 32) {
                         sum_acc[i] = a_acc[i] + b_acc[i];
                         sum_acc[i + 16] = a_acc[i + 16] + b_acc[i + 16];
                       }
                     });
    });
  }
  q.wait();
  auto end = std::chrono::steady_clock::now();
  std::cout << "Vector add5 completed on device - took "
            << (end - start).count() << " u-secs\n";
  return ((end - start).count());
} // end VectorAdd5

上一章                                         主目录    上级目录                                                               下一章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值