RenderScript چارچوبی برای اجرای کارهای محاسباتی فشرده با عملکرد بالا در Android است. RenderScript is primarily oriented for use with data-parallel computation, although serial workloads can benefit as well. زمان اجرای RenderScript کار را در پردازندههای موجود در دستگاه، مانند CPUهای چندهستهای و GPUها، موازی میکند. این کار به شما امکان میدهد بهجای زمانبندی کار، روی بیان الگوریتمها تمرکز کنید. RenderScript بهویژه برای برنامههایی که پردازش تصویر، عکاسی رایانشی، یا بینایی رایانهای انجام میدهند مفید است.
برای شروع کار با RenderScript، دو مفهوم اصلی وجود دارد که باید درک کنید:
- خود زبان زبانی مشتقشده از C99 برای نوشتن کد محاسباتی با عملکرد بالا است. نوشتن هسته RenderScript نحوه استفاده از آن را برای نوشتن هستههای محاسباتی شرح میدهد.
- از میانای برنامهسازی کاربردی کنترل برای مدیریت طول عمر منابع RenderScript و کنترل اجرای هسته استفاده میشود. این چارچوب به سه زبان مختلف دردسترس است: Java، C++ در Android NDK، و خود زبان هسته مشتقشده از C99. استفاده از RenderScript از کد جاوا و RenderScript تکمنبعی بهترتیب گزینههای اول و سوم را توصیف میکنند.
نوشتن هسته RenderScript
هسته RenderScript معمولاً در فایل .rs در فهرست راهنمای
<project_root>/src/rs قرار دارد؛ هر فایل .rs
اسکریپت نامیده میشود. هر دستورات حاوی مجموعه هستهها، توابع، و متغیرهای خودش است. متن میتواند
شامل این موارد باشد:
- یک اعلان دستورالعمل (
#pragma version(1)) که نسخه زبان هسته RenderScript استفادهشده در این دستورگان را اعلام میکند. درحالحاضر، ۱ تنها مقدار معتبر است. - یک اعلان پراگما (
#pragma rs java_package_name(com.example.app)) که نام بسته کلاسهای Java را که از این اسکریپت منعکس شده است اعلام میکند. توجه داشته باشید که فایل.rsشما باید بخشی از بسته برنامهتان باشد و نه در پروژه کتابخانه. - صفر یا چند تابع فراخوانیشدنی. تابع فراخواندنی یک تابع RenderScript تکرشتهای است که میتوانید آن را از کد Java خود با آرگومانهای دلخواه فراخوانی کنید. اینها اغلب برای راهاندازی اولیه یا محاسبات سریالی در خط لوله پردازش بزرگتر مفید هستند.
صفر یا چند متغیر سراسری اسکریپت. متغیر سراسری اسکریپت شبیه متغیر سراسری در C است. میتوانید از کد Java به متغیرهای سراسری دستورگان دسترسی پیدا کنید و این متغیرها اغلب برای ارسال پارامتر به هستههای RenderScript استفاده میشوند. متغیرهای سراسری اسکریپت با جزئیات بیشتر در اینجا توضیح داده شده است.
صفر یا چند هسته محاسباتی. هسته محاسباتی یک تابع یا مجموعهای از توابع است که میتوانید زمان اجرای RenderScript را برای اجرای موازی در مجموعهای از دادهها هدایت کنید. دو نوع هسته محاسباتی وجود دارد: هستههای نگاشت (که هستههای foreach نیز نامیده میشوند) و هستههای کاهش.
هسته نگاشت تابع موازیای است که روی مجموعهای از
Allocationsبا ابعاد یکسان عمل میکند. بهطور پیشفرض، این تابع یکبار برای هر مختصات در آن ابعاد اجرا میشود. معمولاً (اما نه منحصراً) برای تبدیل مجموعهای ازAllocationsورودی بهAllocationخروجی، هر بار یکElement، استفاده میشود.در اینجا نمونهای از یک هسته نگاشت ساده آورده شده است:
uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; }
در بیشتر موارد، این تابع با تابع استاندارد C یکسان است. ویژگی
RS_KERNELاعمالشده بر نمونه اولیه تابع مشخص میکند که تابع بهجای تابع قابلفراخوانی، هسته نگاشت RenderScript است. آرگومانinبهطور خودکار براساس ورودیAllocationکه به راهاندازی هسته ارسال شده است تکمیل میشود. متغیرهای مستقلxوyدر زیر بحث شدهاند. مقدار برگرداندهشده از هسته بهطور خودکار در مکان مناسب در خروجیAllocationنوشته میشود. بهطور پیشفرض، این هسته در سراسر ورودی آن اجرا میشودAllocation، با یک اجرای تابع هسته بهازای هرElementدرAllocation.هسته نگاشت ممکن است یک یا چند
Allocationsورودی، یکAllocationخروجی، یا هر دو را داشته باشد. زمان اجرای RenderScript بررسی میکند تا مطمئن شود که همه «تخصیصهای» ورودی و خروجی ابعاد یکسانی دارند و انواعElement«تخصیصهای» ورودی و خروجی با نمونه اولیه هسته مطابقت دارند؛ اگر هرکدام از این بررسیها ناموفق باشد، RenderScript استثنایی را مطرح میکند.توجه: قبلاز Android 6.0 (سطح میانای برنامهسازی کاربردی ۲۳)، هسته نگاشت ممکن است بیشاز یک ورودی
Allocationنداشته باشد.اگر به ورودی یا خروجی بیشتری
Allocationsنسبتبه هسته نیاز دارید، این اشیاء باید بهrs_allocationسراسریهای اسکریپت مقید شوند و ازطریقrsGetElementAt_type()یاrsSetElementAt_type()از هسته یا تابع قابلفراخوانی به آنها دسترسی پیدا کنید.توجه:
RS_KERNELیک ماکرو است که بهطور خودکار توسط RenderScript برای راحتی شما تعریف شده است:#define RS_KERNEL __attribute__((kernel))
هسته کاهش خانوادهای از توابع است که روی مجموعهای از ورودیهای
Allocationsبا ابعاد یکسان عمل میکند. بهطور پیشفرض، تابع انباشتگر آن یکبار برای هر مختصات در آن ابعاد اجرا میشود. معمولاً (اما نه منحصراً) برای «کاهش» مجموعهای ازAllocationsورودی به یک مقدار واحد استفاده میشود.در اینجا نمونهای از یک هسته کاهش ساده آورده شده است که
Elementsورودی خود را جمع میکند:#pragma rs reduce(addint) accumulator(addintAccum) static void addintAccum(int *accum, int val) { *accum += val; }
هسته کاهش از یک یا چند تابع نوشتهشده توسط کاربر تشکیل شده است.
#pragma rs reduceبرای تعریف هسته با مشخص کردن نام آن (addint، در این مثال) و نامها و نقشهای توابعی که هسته را تشکیل میدهند (تابعaccumulatoraddintAccum، در این مثال) استفاده میشود. همه این توابع بایدstaticباشند. هسته کاهش همیشه به تابعaccumulatorنیاز دارد؛ همچنین ممکن است بسته به کاری که میخواهید هسته انجام دهد، توابع دیگری نیز داشته باشد.تابع انباشتگر هسته کاهش باید
voidرا برگرداند و باید حداقل دو شناسه داشته باشد. آرگومان اول (accum، در این مثال) اشارهگری به مورد داده انباشتگر است و آرگومان دوم (val، در این مثال) بهطور خودکار براساس ورودیAllocationکه به راهاندازی هسته ارسال شده است پر میشود. مورد داده انباشتگر توسط زمان اجرای RenderScript ایجاد میشود؛ بهطور پیشفرض، مقدار اولیه آن صفر است. بهطور پیشفرض، این هسته در سراسر ورودی آن اجرا میشودAllocation، با یک اجرای تابع انباشتگر بهازای هرElementدرAllocation. بهطور پیشفرض، مقدار نهایی مورد داده انباشتگر بهعنوان نتیجه کاهش درنظر گرفته میشود و به Java برگردانده میشود. زمان اجرای RenderScript بررسی میکند تا مطمئن شود نوعElement«تخصیص» ورودی با نمونه اولیه تابع انباشتگر مطابقت داشته باشد؛ اگر مطابقت نداشته باشد، RenderScript استثنایی را ایجاد میکند.یک هسته کاهش دارای یک یا چند ورودی
Allocationsاست اما خروجیAllocationsندارد.هستههای کاهش با جزئیات بیشتر در اینجا توضیح داده شدهاند.
هستههای کاهش در Android 7.0 (سطح میانای برنامهسازی کاربردی ۲۴) و نسخههای بالاتر پشتیبانی میشوند.
تابع هسته نگاشت یا تابع انباشتگر هسته کاهش ممکن است بااستفاده از آرگومانهای ویژه
x،y، وzکه باید از نوعintیاuint32_tباشند، به مختصات اجرای فعلی دسترسی پیدا کند. این آرگومانها اختیاری هستند.یک تابع هسته نگاشت یا یک تابع انباشتگر هسته کاهش نیز ممکن است آرگومان ویژه اختیاری
contextاز نوع rs_kernel_context را بگیرد. این مورد برای خانوادهای از میاناهای برنامهسازی کاربردی زمان اجرا لازم است که برای پُرسمان برخیاز ویژگیهای اجرای فعلی استفاده میشوند -- برای مثال، rsGetDimX. (آرگومانcontextدر Android نسخه ۶.۰ (سطح میانای برنامهسازی کاربردی ۲۳) و بالاتر دردسترس است.)- تابع اختیاری
init(). تابعinit()نوع خاصی از تابع قابلفراخوانی است که RenderScript هنگام اولین نمونهسازی اسکریپت اجرا میکند. این کار باعث میشود برخی محاسبات بهطور خودکار در زمان ایجاد دستورگان انجام شود. - صفر یا چند تابع و سراسری اسکریپت ایستا. یک اسکریپت سراسری ایستا معادل یک
اسکریپت سراسری است، با این تفاوت که از کد جاوا قابل دسترسی نیست. تابع ایستا یک تابع استاندارد C است که میتواند از هر هسته یا تابع قابلفراخوانی در اسکریپت فراخوانی شود، اما در Java API نمایش داده نمیشود. اگر نیازی نیست که از کد Java به تابع یا سراسری اسکریپت دسترسی پیدا شود،
اکیداً توصیه میشود که
staticاعلام شود.
تنظیم دقت نقطه شناور
میتوانید سطح دقت نقطه شناور موردنیاز را در دستورگان کنترل کنید. این گزینه زمانی مفید است که استاندارد کامل IEEE 754-2008 (که بهطور پیشفرض استفاده میشود) لازم نباشد. دستورات پیشپردازنده زیر میتوانند سطح متفاوتی از دقت ممیز شناور را تنظیم کنند:
#pragma rs_fp_full(پیشفرض اگر چیزی مشخص نشده باشد): برای برنامههایی که به دقت ممیز شناور طبق استاندارد IEEE 754-2008 نیاز دارند.#pragma rs_fp_relaxed: برای برنامههایی که به رعایت دقیق IEEE 754-2008 نیاز ندارند و میتوانند دقت کمتری را تحمل کنند. این حالت، «فلاش به صفر» را برای غیرنرمالها و «گرد کردن به صفر» فعال میکند.#pragma rs_fp_imprecise: برای برنامههایی که الزامات دقت سختگیرانهای ندارند. این حالت همه چیز را درrs_fp_relaxedبههمراه موارد زیر فعال میکند:- عملیاتی که نتیجه آن 0.0- است میتواند بهجای آن 0.0+ را برگرداند.
- عملیات روی INF و NAN تعریف نشده است.
اکثر برنامهها میتوانند بدون هیچ عارضه جانبی از rs_fp_relaxed استفاده کنند. این امر ممکن است در برخی معماریها بهدلیل بهینهسازیهای اضافی که فقط با دقت کاهشیافته دردسترس هستند (مانند دستورالعملهای SIMD CPU) بسیار مفید باشد.
دسترسی به RenderScript API از Java
هنگام توسعه برنامه Android که از RenderScript استفاده میکند، میتوانید به یکی از دو روش زیر از Java به میانای برنامهسازی کاربردی آن دسترسی پیدا کنید:
android.renderscript- میاناهای برنامهسازی کاربردی در این بسته کلاس در دستگاههای دارای Android نسخه ۳.۰ (سطح میانای برنامهسازی کاربردی ۱۱) و بالاتر دردسترس است.android.support.v8.renderscript- میاناهای برنامهسازی کاربردی در این بسته ازطریق کتابخانه پشتیبانی دردسترس هستند که به شما امکان میدهد از آنها در دستگاههای دارای Android نسخه ۲.۳ (سطح میانای برنامهسازی کاربردی ۹) و بالاتر استفاده کنید.
معاوضهها در اینجا آمده است:
- اگر از Support Library APIs استفاده میکنید، بخش RenderScript برنامه شما با دستگاههای دارای Android نسخه ۲.۳ (سطح میانای برنامهسازی کاربردی ۹) و بالاتر سازگار خواهد بود، صرفنظر از اینکه از کدام ویژگیهای RenderScript استفاده میکنید. این کار به برنامهتان اجازه میدهد در دستگاههای بیشتری نسبتبه زمانی که از میاناهای برنامهسازی کاربردی
بومی (
android.renderscript) استفاده میکنید کار کند. - برخیاز ویژگیهای RenderScript ازطریق میاناهای برنامهسازی کاربردی «کتابخانه پشتیبانی» دردسترس نیستند.
- اگر از میاناهای برنامهسازی کاربردی «کتابخانه پشتیبانی» استفاده کنید، فایلهای APK بزرگتری (احتمالاً بهطور قابلتوجهی) نسبت به زمانی که از میاناهای برنامهسازی کاربردی بومی (
android.renderscript) استفاده میکنید دریافت خواهید کرد.
استفاده از میاناهای برنامهسازی کاربردی RenderScript Support Library
برای استفاده از میاناهای برنامهسازی کاربردی RenderScript در «کتابخانه پشتیبانی»، باید محیط توسعه خود را پیکربندی کنید تا بتوانید به آنها دسترسی داشته باشید. ابزارهای کیت توسعه نرمافزار Android زیر برای استفاده از این میاناهای برنامهسازی کاربردی لازم است:
- ابزارهای کیت توسعه نرمافزار Android نسخه ۲۲.۲ یا بالاتر
- کیت توسعه نرمافزار Android نسخه ۱۸.۱.۰ یا بالاتر
توجه داشته باشید که از Android SDK Build-tools 24.0.0 به بعد، Android 2.2 (سطح API ۸) دیگر پشتیبانی نمیشود.
میتوانید نسخه نصبشده این ابزارها را در مدیر کیت توسعه نرمافزار Android بررسی و بهروز کنید.
برای استفاده از میاناهای برنامهسازی کاربردی RenderScript در «کتابخانه پشتیبانی»:
- مطمئن شوید نسخه لازم کیت توسعه نرمافزار Android را نصب کردهاید.
- تنظیمات فرایند ساخت Android را بهروز کنید تا تنظیمات RenderScript را دربرگیرد:
- فایل
build.gradleرا در پوشه برنامه واحد برنامهتان باز کنید. - تنظیمات RenderScript زیر را به فایل اضافه کنید:
شیک
android { compileSdkVersion 36 defaultConfig { minSdkVersion 9 targetSdkVersion 19 renderscriptTargetApi 18 renderscriptSupportModeEnabled true } }
کاتلین
android { compileSdkVersion(36) defaultConfig { minSdkVersion(9) targetSdkVersion(19) renderscriptTargetApi = 18 renderscriptSupportModeEnabled = true } }
تنظیمات فهرستشده در بالا رفتار خاصی را در فرایند ساخت Android کنترل میکنند:
renderscriptTargetApi- نسخه بایتکد را برای تولید مشخص میکند. توصیه میکنیم این مقدار را روی پایینترین سطح API که میتواند همه عملکردهای مورداستفاده شما را ارائه دهد تنظیم کنید وrenderscriptSupportModeEnabledرا رویtrueتنظیم کنید. مقادیر معتبر برای این تنظیم هر مقدار عدد صحیحی از ۱۱ تا جدیدترین سطح API منتشرشده است. اگر حداقل نسخه کیت توسعه نرمافزار مشخصشده در مانیفست برنامه روی مقدار دیگری تنظیم شده باشد، آن مقدار نادیده گرفته میشود و از مقدار هدف در فایل ساخت برای تنظیم حداقل نسخه کیت توسعه نرمافزار استفاده میشود.renderscriptSupportModeEnabled- مشخص میکند که اگر دستگاهی که کدبایت تولیدشده روی آن اجرا میشود از نسخه هدف پشتیبانی نکند، کدبایت باید به نسخه سازگار برگردد.
- فایل
- در کلاسهای برنامهتان که از RenderScript استفاده میکنند، یک واردات برای کلاسهای «کتابخانه پشتیبانی» اضافه کنید:
کاتلین
import android.support.v8.renderscript.*
جاوا
import android.support.v8.renderscript.*;
استفاده کردن از RenderScript از کد Java یا Kotlin
استفاده از RenderScript از کد Java یا Kotlin به کلاسهای API واقع در بسته
android.renderscript یا android.support.v8.renderscript متکی است. اکثر
برنامهها از الگوی استفاده پایه یکسانی پیروی میکنند:
- مقداردهی اولیه بافت RenderScript. بافت
RenderScript، که باcreate(Context)ایجاد شده است، تضمین میکند که RenderScript قابلاستفاده است و شیئی را برای کنترل طول عمر همه اشیای RenderScript بعدی ارائه میدهد. باید ایجاد بافت را بهعنوان عملیاتی درنظر بگیرید که ممکن است طولانی باشد، زیرا ممکن است منابعی را در قطعات سختافزاری مختلف ایجاد کند؛ درصورت امکان، نباید در مسیر بحرانی برنامه باشد. معمولاً یک برنامه در هر زمان فقط یک بافت RenderScript دارد. - حداقل یک
Allocationایجاد کنید تا به دستورگان منتقل شود.Allocationیک شیء RenderScript است که فضای ذخیرهسازی برای مقدار ثابتی از دادهها فراهم میکند. هستهها در اسکریپتها اشیاءAllocationرا بهعنوان ورودی و خروجی خود میگیرند و اشیاءAllocationمیتوانند در هستهها بااستفاده ازrsGetElementAt_type()وrsSetElementAt_type()وقتی بهعنوان متغیرهای سراسری اسکریپت محدود شدهاند، دسترسی پیدا کنند. اشیاءAllocationامکان میدهند آرایهها از کد Java به کد RenderScript و برعکس منتقل شوند. اشیاءAllocationمعمولاً بااستفاده ازcreateTyped()یاcreateFromBitmap()ایجاد میشوند. - هر اسکریپتی را که لازم است ایجاد کنید. دو نوع اسکریپت دردسترس است
که میتوانید هنگام استفاده از RenderScript از آنها استفاده کنید:
- ScriptC: اینها اسکریپتهای تعریفشده توسط کاربر هستند که در بخش نوشتن هسته RenderScript در بالا توضیح داده شدهاند. هر متن یک کلاس Java دارد
که توسط کامپایلر RenderScript منعکس میشود تا دسترسی به متن از کد Java آسان شود؛
این کلاس نام
ScriptC_filenameرا دارد. برای مثال، اگر هسته نگاشت بالا درinvert.rsقرار داشته باشد و بافت RenderScript ازقبل درmRenderScriptقرار داشته باشد، کد Java یا Kotlin برای نمونهسازی دستورگان بهصورت زیر خواهد بود:کاتلین
val invert = ScriptC_invert(renderScript)
جاوا
ScriptC_invert invert = new ScriptC_invert(renderScript);
- ScriptIntrinsic: اینها هستههای RenderScript داخلی برای عملیات رایج هستند،
مانند محو کردن گاوسی، پیچش، و ترکیب تصویر. برای اطلاعات بیشتر، زیرکلاسهای
ScriptIntrinsicرا ببینید.
- ScriptC: اینها اسکریپتهای تعریفشده توسط کاربر هستند که در بخش نوشتن هسته RenderScript در بالا توضیح داده شدهاند. هر متن یک کلاس Java دارد
که توسط کامپایلر RenderScript منعکس میشود تا دسترسی به متن از کد Java آسان شود؛
این کلاس نام
- «تخصیصها» را با داده پر کنید. بهجز «تخصیصهای» ایجادشده با
createFromBitmap()، «تخصیص» هنگام ایجاد اولیه با دادههای خالی پر میشود. برای پر کردن «تخصیص»، از یکی از روشهای «کپی» درAllocationاستفاده کنید. روشهای «کپی» همگام هستند. - متغیرهای سراسری اسکریپت لازم را تنظیم کنید. میتوانید متغیرهای سراسری را بااستفاده از روشهای موجود در
همان کلاس
ScriptC_filenameبا نامset_globalnameتنظیم کنید. برای مثال، برای تنظیم متغیرintبا نامthreshold، از روش جاواset_threshold(int)استفاده کنید؛ و برای تنظیم متغیرrs_allocationبا نامlookup، از روش جاواset_lookup(Allocation)استفاده کنید. روشهایsetناهمگام هستند. - هستهها و توابع قابلفراخوانی مناسب را راهاندازی کنید.
روشهای راهاندازی هسته دادهشده در همان کلاس
ScriptC_filenameبا روشهایی بهنامforEach_mappingKernelName()یاreduce_reductionKernelName()منعکس میشوند. این راهاندازیها ناهمگام هستند. بسته به آرگومانهای هسته، روش یک یا چند تخصیص را میگیرد که همه آنها باید ابعاد یکسانی داشته باشند. بهطور پیشفرض، هسته روی هر مختصات در آن ابعاد اجرا میشود؛ برای اجرای هسته روی زیرمجموعهای از آن مختصات،Script.LaunchOptionsمناسبی را بهعنوان آخرین متغیر مستقل به روشforEachیاreduceارسال کنید.توابع قابلفراخوانی را بااستفاده از روشهای
invoke_functionNameمنعکسشده در همان کلاسScriptC_filenameراهاندازی کنید. این راهاندازیها ناهمگام هستند. - بازیابی دادهها از اشیاء
Allocationو اشیاء javaFutureType. برای دسترسی به دادههایAllocationاز کد Java، باید آن دادهها را بااستفاده از یکی از روشهای «کپی» درAllocationبه Java برگردانید. برای بهدست آوردن نتیجه هسته کاهش، باید از روشjavaFutureType.get()استفاده کنید. روشهای «کپی» وget()همگام هستند. - بافت RenderScript را ازهم جدا کنید. میتوانید بااستفاده از
destroy()یا با اجازه دادن به شیء بافت RenderScript برای جمعآوری زباله، بافت RenderScript را ازبین ببرید. این باعث میشود هرگونه استفاده بیشتر از هر شیء متعلق به آن زمینه، استثنایی را ایجاد کند.
مدل اجرای ناهمگام
روشهای منعکسشده forEach، invoke، reduce،
و set ناهمگام هستند -- هرکدام ممکن است قبلاز تکمیل کنش
درخواستشده به Java برگردند. بااینحال، کنشهای فردی بهترتیبی که راهاندازی شدهاند سریالی میشوند.
کلاس Allocation روشهای «کپی» را برای کپی کردن دادهها به
و از «تخصیصها» ارائه میدهد. روش «کپی» همگام است و نسبت به هریک از کنشهای ناهمگام بالا که به همان «تخصیص» مربوط میشود، سریالی است.
کلاسهای javaFutureType منعکسشده
یک روش get() برای بهدست آوردن نتیجه کاهش ارائه میدهند. get() همگام است و نسبت به کاهش (که ناهمگام است) سریالی شده است.
RenderScript تکمنبعی
Android 7.0 (سطح API ۲۴) ویژگی برنامهنویسی جدیدی بهنام Single-Source
RenderScript معرفی میکند که در آن هستهها از اسکریپتی که در آن تعریف شدهاند راهاندازی میشوند، نه
از Java. این رویکرد درحالحاضر به هستههای نگاشت محدود است که برای اختصار در این بخش بهسادگی «هسته» نامیده میشوند. این ویژگی جدید همچنین از ایجاد تخصیصهای نوع
rs_allocation از داخل اسکریپت پشتیبانی میکند. اکنون میتوان کل الگوریتم را فقط در یک اسکریپت پیادهسازی کرد، حتی اگر چندین راهاندازی هسته لازم باشد.
این کار دو مزیت دارد: کد خواناتر، زیرا پیادهسازی الگوریتم را در یک زبان نگه میدارد؛ و کد احتمالاً سریعتر، بهدلیل انتقالهای کمتر بین Java و RenderScript در چندین راهاندازی هسته.
در RenderScript تکمنبعی، هستهها را همانطور که در
نوشتن هسته RenderScript شرح داده شده است مینویسید. سپس تابع فراخوانیپذیری مینویسید که
rsForEach() را برای راهاندازی آنها فراخوانی میکند. این میانای برنامهسازی کاربردی یک تابع هسته را بهعنوان پارامتر اول میگیرد، و بهدنبال آن تخصیصهای ورودی و خروجی را دریافت میکند. میانای برنامهسازی کاربردی مشابهی
rsForEachWithOptions() متغیر مستقل اضافهای از نوع
rs_script_call_t میگیرد که زیرمجموعهای از عناصر را از تخصیصهای ورودی و
خروجی برای پردازش تابع هسته مشخص میکند.
برای شروع محاسبات RenderScript، تابع قابلفراخوانی را از Java فراخوانی میکنید.
مراحل استفاده از RenderScript از کد Java را دنبال کنید.
در مرحله راهاندازی هستههای مناسب، تابع فراخوانیشدنی را بااستفاده از invoke_function_name() فراخوانی کنید که کل محاسبات را، ازجمله راهاندازی هستهها، شروع خواهد کرد.
اغلب برای ذخیره و ارسال نتایج واسط از یک راهاندازی هسته به راهاندازی دیگر، به تخصیص نیاز است. میتوانید آنها را بااستفاده از
rsCreateAllocation() ایجاد کنید. یکی از شکلهای آسان برای استفاده از آن API،
rsCreateAllocation_<T><W>(…) است که در آن T نوع داده برای یک
عنصر است و W عرض بردار برای عنصر است. این میانای برنامهسازی کاربردی اندازهها را در
ابعاد X، Y، و Z بهعنوان متغیر مستقل دریافت میکند. برای تخصیصهای یکبعدی یا دوبعدی، اندازه بُعد Y یا Z را میتوان
حذف کرد. برای مثال، rsCreateAllocation_uchar4(16384) یک تخصیص یکبعدی از
۱۶۳۸۴ عنصر ایجاد میکند که هرکدام از نوع uchar4 هستند.
تخصیصها بهطور خودکار توسط سیستم مدیریت میشود. لازم نیست
آنها را بهطور صریح منتشر یا آزاد کنید. بااینحال، میتوانید
rsClearObject(rs_allocation* alloc) را فراخوانی کنید تا نشان دهید دیگر به دسته
alloc برای تخصیص اساسی نیاز ندارید،
تا سیستم بتواند منابع را در اسرع وقت آزاد کند.
بخش نوشتن هسته RenderScript حاوی نمونهای از
هسته است که تصویر را وارونه میکند. مثال زیر آن را گسترش میدهد تا بیش از یک جلوه را به یک تصویر اعمال کند،
بااستفاده از RenderScript تکمنبعی. این شامل هسته دیگری، greyscale، است که
تصویر رنگی را به سیاه و سفید تبدیل میکند. سپس یک تابع قابلفراخوانی process() آن دو هسته را بهطور متوالی روی تصویر ورودی اعمال میکند و تصویر خروجی را تولید میکند. تخصیصهای ورودی و خروجی بهعنوان متغیرهای مستقل از نوع
rs_allocation ارسال میشوند.
// File: singlesource.rs #pragma version(1) #pragma rs java_package_name(com.android.rssample) static const float4 weight = {0.299f, 0.587f, 0.114f, 0.0f}; uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; } uchar4 RS_KERNEL greyscale(uchar4 in) { const float4 inF = rsUnpackColor8888(in); const float4 outF = (float4){ dot(inF, weight) }; return rsPackColorTo8888(outF); } void process(rs_allocation inputImage, rs_allocation outputImage) { const uint32_t imageWidth = rsAllocationGetDimX(inputImage); const uint32_t imageHeight = rsAllocationGetDimY(inputImage); rs_allocation tmp = rsCreateAllocation_uchar4(imageWidth, imageHeight); rsForEach(invert, inputImage, tmp); rsForEach(greyscale, tmp, outputImage); }
میتوانید تابع process() را از Java یا Kotlin بهصورت زیر فراخوانی کنید:
کاتلین
val RS: RenderScript = RenderScript.create(context) val script = ScriptC_singlesource(RS) val inputAllocation: Allocation = Allocation.createFromBitmapResource( RS, resources, R.drawable.image ) val outputAllocation: Allocation = Allocation.createTyped( RS, inputAllocation.type, Allocation.USAGE_SCRIPT or Allocation.USAGE_IO_OUTPUT ) script.invoke_process(inputAllocation, outputAllocation)
جاوا
// File SingleSource.java RenderScript RS = RenderScript.create(context); ScriptC_singlesource script = new ScriptC_singlesource(RS); Allocation inputAllocation = Allocation.createFromBitmapResource( RS, getResources(), R.drawable.image); Allocation outputAllocation = Allocation.createTyped( RS, inputAllocation.getType(), Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT); script.invoke_process(inputAllocation, outputAllocation);
این مثال نشان میدهد که چگونه الگوریتمی که شامل دو راهاندازی هسته است میتواند بهطور کامل در خود زبان RenderScript پیادهسازی شود. بدون RenderScript تکمنبعی، باید هر دو هسته را از کد Java راهاندازی کنید، راهاندازیهای هسته را از تعاریف هسته جدا کنید، و درک کل الگوریتم را دشوارتر کنید. Not only is the Single-Source RenderScript code easier to read, it also eliminates the transitioning between Java and the script across kernel launches. برخیاز الگوریتمهای تکراری ممکن است هستهها را صدها بار راهاندازی کنند، که باعث میشود سربار چنین انتقالی قابلتوجه باشد.
متغیرهای سراسری متن
متغیر سراسری دستورگان یک متغیر سراسری معمولی غیرstatic
در فایل دستورگان (.rs) است. برای یک اسکریپت
جهانی با نام var که در
فایل filename.rs تعریف شده است، یک
روش get_var وجود خواهد داشت که در
کلاس ScriptC_filename منعکس میشود. مگر اینکه متغیر سراسری
const باشد، دراینصورت
روش set_var نیز وجود خواهد داشت.
یک سراسری اسکریپت دادهشده دو مقدار جداگانه دارد -- یک مقدار Java و یک مقدار اسکریپت. این مقادیر بهصورت زیر عمل میکنند:
- اگر var مقداردهی اولیه ایستا در دستورگان داشته باشد، مقدار اولیه var را هم در Java و هم در دستورگان مشخص میکند. درغیراینصورت، مقدار اولیه صفر است.
- دسترسی به var در متن، مقدار متن آن را میخواند و مینویسد.
- روش
get_varمقدار Java را میخواند. - روش
set_var(درصورت وجود) مقدار Java را فوراً مینویسد و مقدار اسکریپت را بهصورت ناهمگام مینویسد.
توجه: این یعنی بهجز هر مقداردهی اولیه ایستا در اسکریپت، مقادیر نوشتهشده در یک متغیر سراسری از داخل اسکریپت برای Java قابلمشاهده نیستند.
هستههای کاهش در عمق
کاهش فرایند ترکیب مجموعهای از دادهها در یک مقدار واحد است. این یک دستور اولیه مفید در برنامهنویسی موازی است، با کاربردهایی مانند موارد زیر:
- محاسبه مجموع یا حاصلضرب روی همه دادهها
- محاسبه عملیات منطقی (
and،or،xor) روی همه دادهها - پیدا کردن کمترین یا بیشترین مقدار در دادهها
- جستجوی مقدار خاص یا مختصات مقدار خاص در دادهها
در Android 7.0 (سطح میانای برنامهسازی کاربردی ۲۴) و نسخههای جدیدتر، RenderScript از هستههای کاهش پشتیبانی میکند تا الگوریتمهای کاهش کارآمد نوشتهشده توسط کاربر را مجاز کند. میتوانید هستههای کاهش را در ورودیهای دارای ۱، ۲، یا ۳ بُعد راهاندازی کنید.
نمونهای در بالا هسته کاهش addint سادهای را نشان میدهد.
در اینجا یک هسته کاهش پیچیدهتر findMinAndMax وجود دارد
که مکانهای حداقل و حداکثر مقادیر long را در یک
Allocation یکبعدی پیدا میکند:
#define LONG_MAX (long)((1UL << 63) - 1) #define LONG_MIN (long)(1UL << 63) #pragma rs reduce(findMinAndMax) \ initializer(fMMInit) accumulator(fMMAccumulator) \ combiner(fMMCombiner) outconverter(fMMOutConverter) // Either a value and the location where it was found, or INITVAL. typedef struct { long val; int idx; // -1 indicates INITVAL } IndexedVal; typedef struct { IndexedVal min, max; } MinAndMax; // In discussion below, this initial value { { LONG_MAX, -1 }, { LONG_MIN, -1 } } // is called INITVAL. static void fMMInit(MinAndMax *accum) { accum->min.val = LONG_MAX; accum->min.idx = -1; accum->max.val = LONG_MIN; accum->max.idx = -1; } //---------------------------------------------------------------------- // In describing the behavior of the accumulator and combiner functions, // it is helpful to describe hypothetical functions // IndexedVal min(IndexedVal a, IndexedVal b) // IndexedVal max(IndexedVal a, IndexedVal b) // MinAndMax minmax(MinAndMax a, MinAndMax b) // MinAndMax minmax(MinAndMax accum, IndexedVal val) // // The effect of // IndexedVal min(IndexedVal a, IndexedVal b) // is to return the IndexedVal from among the two arguments // whose val is lesser, except that when an IndexedVal // has a negative index, that IndexedVal is never less than // any other IndexedVal; therefore, if exactly one of the // two arguments has a negative index, the min is the other // argument. Like ordinary arithmetic min and max, this function // is commutative and associative; that is, // // min(A, B) == min(B, A) // commutative // min(A, min(B, C)) == min((A, B), C) // associative // // The effect of // IndexedVal max(IndexedVal a, IndexedVal b) // is analogous (greater . . . never greater than). // // Then there is // // MinAndMax minmax(MinAndMax a, MinAndMax b) { // return MinAndMax(min(a.min, b.min), max(a.max, b.max)); // } // // Like ordinary arithmetic min and max, the above function // is commutative and associative; that is: // // minmax(A, B) == minmax(B, A) // commutative // minmax(A, minmax(B, C)) == minmax((A, B), C) // associative // // Finally define // // MinAndMax minmax(MinAndMax accum, IndexedVal val) { // return minmax(accum, MinAndMax(val, val)); // } //---------------------------------------------------------------------- // This function can be explained as doing: // *accum = minmax(*accum, IndexedVal(in, x)) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // *accum is INITVAL, then this function sets // *accum = IndexedVal(in, x) // // After this function is called, both accum->min.idx and accum->max.idx // will have nonnegative values: // - x is always nonnegative, so if this function ever sets one of the // idx fields, it will set it to a nonnegative value // - if one of the idx fields is negative, then the corresponding // val field must be LONG_MAX or LONG_MIN, so the function will always // set both the val and idx fields static void fMMAccumulator(MinAndMax *accum, long in, int x) { IndexedVal me; me.val = in; me.idx = x; if (me.val <= accum->min.val) accum->min = me; if (me.val >= accum->max.val) accum->max = me; } // This function can be explained as doing: // *accum = minmax(*accum, *val) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // one of the two accumulator data items is INITVAL, then this // function sets *accum to the other one. static void fMMCombiner(MinAndMax *accum, const MinAndMax *val) { if ((accum->min.idx < 0) || (val->min.val < accum->min.val)) accum->min = val->min; if ((accum->max.idx < 0) || (val->max.val > accum->max.val)) accum->max = val->max; } static void fMMOutConverter(int2 *result, const MinAndMax *val) { result->x = val->min.idx; result->y = val->max.idx; }
توجه: نمونههای بیشتری از هستههای کاهش در اینجا وجود دارد.
برای اجرای هسته کاهش، زمان اجرای RenderScript یک یا چند
متغیر بهنام مورد دادههای
انباشتگر ایجاد میکند تا وضعیت فرایند کاهش را نگه دارد. زمان اجرای RenderScript
تعداد موارد دادههای انباشتگر را بهگونهای انتخاب میکند که عملکرد را به حداکثر برساند. نوع
موارد داده انباشتگر (accumType) توسط تابع
انباشتگر هسته تعیین میشود -- اولین آرگومان آن تابع اشارهگری به مورد داده
انباشتگر است. بهطور پیشفرض، هر مورد داده انباشتگر به صفر مقداردهی اولیه میشود (گویی
توسط memset)؛ بااینحال، میتوانید یک تابع مقداردهی اولیه بنویسید تا کار
متفاوتی انجام دهد.
مثال: در هسته addint، از موارد داده انباشتگر (از نوع int) برای جمع کردن مقادیر ورودی استفاده میشود. تابع مقداردهی اولیه وجود ندارد، بنابراین هر مورد داده انباشتگر روی صفر مقداردهی اولیه میشود.
مثال: در
هسته findMinAndMax، از موارد داده انباشتگر
(از نوع MinAndMax) برای پیگیری کمترین و بیشترین مقادیر
یافتشده تاکنون استفاده میشود. یک تابع مقداردهی اولیه وجود دارد که اینها را بهترتیب روی LONG_MAX و LONG_MIN تنظیم میکند؛ و مکان این مقادیر را روی ۱- تنظیم میکند که نشان میدهد این مقادیر درواقع در بخش (خالی) ورودی که پردازش شده است وجود ندارند.
RenderScript تابع انباشتگر شما را یکبار برای هر مختصات در ورودی(ها) فراخوانی میکند. معمولاً تابع شما باید بهنحوی عنصر داده انباشتگر را براساس ورودی بهروز کند.
مثال: در هسته addint، تابع انباشتگر مقدار «عنصر» ورودی را به مورد داده انباشتگر اضافه میکند.
مثال: در هسته findMinAndMax، تابع انباشتگر بررسی میکند که آیا مقدار عنصر ورودی کمتر یا مساوی با حداقل مقدار ثبتشده در عنصر داده انباشتگر است و/یا بیشتر یا مساوی با حداکثر مقدار ثبتشده در عنصر داده انباشتگر است، و عنصر داده انباشتگر را براساس آن بهروزرسانی میکند.
پساز اینکه تابع انباشتگر یکبار برای هر مختصات در ورودی(ها) فراخوانی شد، RenderScript باید موارد داده انباشتگر را باهم ترکیب کند و به یک مورد داده انباشتگر واحد تبدیل کند. برای انجام این کار، میتوانید تابع ترکیبکننده بنویسید. اگر تابع انباشتگر یک ورودی واحد داشته باشد و آرگومانهای ویژه نداشته باشد، نیازی نیست تابع ترکیبکننده بنویسید؛ RenderScript از تابع انباشتگر برای ترکیب کردن موارد داده انباشتگر استفاده خواهد کرد. (اگر این رفتار پیشفرض چیزی نیست که میخواهید، همچنان میتوانید تابع ترکیبکننده بنویسید.)
مثال: در هسته addint، هیچ تابع ترکیبکنندهای وجود ندارد، بنابراین از تابع انباشتگر استفاده خواهد شد. این رفتار صحیح است، زیرا اگر مجموعهای از مقادیر را به دو بخش تقسیم کنیم و مقادیر آن دو بخش را بهطور جداگانه جمع کنیم، جمع کردن آن دو مجموع با جمع کردن کل مجموعه یکسان است.
مثال: در هسته findMinAndMax، تابع ترکیبکننده بررسی میکند که آیا کمترین مقدار ثبتشده در عنصر داده انباشتگر «مبدأ» *val کمتر از کمترین مقدار ثبتشده در عنصر داده انباشتگر «مقصد» *accum است یا نه، و *accum را براساس آن بهروز میکند. برای حداکثر مقدار نیز همین کار را انجام میدهد. این کار *accum را به وضعیتی بهروز میکند که اگر همه مقادیر ورودی بهجای اینکه برخی در *accum و برخی در *val جمع شوند، در *accum جمع شده بودند، میداشت.
پساز اینکه همه موارد دادههای انباشتگر ترکیب شدند، RenderScript نتیجه کاهش را برای بازگشت به Java تعیین میکند. برای انجام این کار میتوانید تابع تبدیلگر خروجی بنویسید. اگر میخواهید مقدار نهایی موارد داده انباشتگر ترکیبی نتیجه کاهش باشد، نیازی به نوشتن تابع تبدیلگر خروجی ندارید.
مثال: در هسته addint، تابع outconverter وجود ندارد. مقدار نهایی موارد داده ترکیبی مجموع همه عناصر ورودی است که همان مقداری است که میخواهیم برگردانیم.
مثال: در
هسته findMinAndMax، تابع outconverter
مقدار نتیجه int2 را مقداردهی اولیه میکند تا مکانهای مقادیر حداقل و
حداکثر حاصل از ترکیب همه موارد دادههای انباشتگر را نگه دارد.
نوشتن هسته کاهش
#pragma rs reduce هسته کاهش را با مشخص کردن نام آن و نامها و نقشهای توابعی که هسته را تشکیل میدهند تعریف میکند. همه این توابع باید
static باشند. هسته کاهش همیشه به تابع accumulator نیاز دارد؛ بسته به کاری که میخواهید هسته انجام دهد، میتوانید برخی یا همه توابع دیگر را حذف کنید.
#pragma rs reduce(kernelName) \ initializer(initializerName) \ accumulator(accumulatorName) \ combiner(combinerName) \ outconverter(outconverterName)
معنی موارد موجود در #pragma بهشرح زیر است:
reduce(kernelName)(اجباری): مشخص میکند که هسته کاهش درحال تعریف شدن است. یک روش Java منعکسشدهreduce_kernelNameهسته را راهاندازی خواهد کرد.initializer(initializerName)(اختیاری): نام تابع مقداردهی اولیه را برای این هسته کاهش مشخص میکند. وقتی هسته را راهاندازی میکنید، RenderScript این تابع را یکبار برای هر مورد داده انباشتگر فراخوانی میکند. تابع باید به این صورت تعریف شود:static void initializerName(accumType *accum) { … }
accumاشارهگری به مورد داده انباشتگر برای این تابع است تا مقداردهی اولیه شود.اگر تابع مقداردهی اولیه ارائه نکنید، RenderScript هر مورد داده انباشتگر را روی صفر مقداردهی اولیه میکند (گویی با
memset)، و بهگونهای رفتار میکند که گویی تابع مقداردهی اولیهای وجود دارد که شبیه این است:static void initializerName(accumType *accum) { memset(accum, 0, sizeof(*accum)); }
accumulator(accumulatorName)(اجباری): نام تابع انباشتگر را برای این هسته کاهش مشخص میکند. وقتی هسته را راهاندازی میکنید، RenderScript این تابع را یکبار برای هر مختصات در ورودی(ها) فراخوانی میکند تا بهنحوی مطابق با ورودی(ها) یک مورد داده انباشتگر را بهروز کند. تابع باید اینگونه تعریف شود:static void accumulatorName(accumType *accum, in1Type in1, …, inNType inN [, specialArguments]) { … }
accumاشارهگری به یک مورد داده انباشتگر برای این تابع است تا اصلاح شود.in1تاinNیک یا چند متغیر مستقل هستند که بهطور خودکار براساس ورودیهای ارسالشده به راهاندازی هسته تکمیل میشوند، یک متغیر مستقل برای هر ورودی. تابع انباشتگر میتواند بهصورت اختیاری هرکدام از آرگومانهای ویژه را بگیرد.یک هسته نمونه با چندین ورودی
dotProductاست.combiner(combinerName)(اختیاری): نام تابع ترکیبکننده را برای این هسته کاهش مشخص میکند. پساز اینکه RenderScript تابع انباشتگر را یکبار برای هر مختصات در ورودی(ها) فراخوانی کرد، این تابع را بهتعداد دفعات لازم فراخوانی میکند تا همه موارد داده انباشتگر را در یک مورد داده انباشتگر واحد ترکیب کند. تابع باید بهاینصورت تعریف شود:
static void combinerName(accumType *accum, const accumType *other) { … }
accumاشارهگری به یک مورد داده انباشتگر «مقصد» برای این تابع است تا آن را اصلاح کند. otherاشارهگری به عنصر داده انباشتگر «منبع» است تا این تابع آن را در*accum«ترکیب» کند.توجه: ممکن است که
*accum،*other، یا هر دو مقداردهی اولیه شده باشند اما هرگز به تابع انباشتگر ارسال نشده باشند؛ یعنی، یک یا هر دو هرگز براساس هیچ داده ورودی بهروز نشده باشند. برای مثال، در هسته findMinAndMax، تابع ترکیبکنندهfMMCombinerبهطور صریحidx < 0را بررسی میکند زیرا نشاندهنده چنین عنصر داده انباشتگری است که مقدار آن INITVAL است.اگر تابع ترکیبکننده ارائه نکنید، RenderScript از تابع انباشتگر بهجای آن استفاده میکند و طوری رفتار میکند که انگار تابع ترکیبکنندهای وجود دارد که شبیه این است:
static void combinerName(accumType *accum, const accumType *other) { accumulatorName(accum, *other); }
اگر هسته بیشاز یک ورودی داشته باشد، اگر نوع داده ورودی با نوع داده انباشتگر یکسان نباشد، یا اگر تابع انباشتگر یک یا چند آرگومان ویژه داشته باشد، تابع ترکیبکننده الزامی است.
outconverter(outconverterName)(اختیاری): نام تابع outconverter را برای این هسته کاهش مشخص میکند. پساز اینکه RenderScript همه موارد دادههای انباشتگر را ترکیب کرد، این تابع را فراخوانی میکند تا نتیجه کاهش را برای بازگشت به Java تعیین کند. تابع باید بهاینصورت تعریف شود:static void outconverterName(resultType *result, const accumType *accum) { … }
resultاشارهگری به یک عنصر داده نتیجه است (اختصاص داده شده اما توسط زمان اجرای RenderScript مقداردهی اولیه نشده است) تا این تابع با نتیجه کاهش مقداردهی اولیه شود. resultType نوع آن عنصر داده است که نیازی نیست با accumType یکسان باشد. accumاشارهگری به آخرین مورد داده انباشتگر است که توسط تابع ترکیبکننده محاسبه شده است.اگر تابع outconverter را ارائه نکنید، RenderScript عنصر داده انباشتگر نهایی را در عنصر داده نتیجه کپی میکند و طوری رفتار میکند که گویی تابع outconverter وجود دارد که شبیه به این است:
static void outconverterName(accumType *result, const accumType *accum) { *result = *accum; }
اگر نوع نتیجهای متفاوت از نوع داده انباشتگر میخواهید، تابع outconverter الزامی است.
توجه داشته باشید که هسته دارای انواع ورودی، نوع عنصر داده انباشتگر، و نوع نتیجه است،
که هیچکدام لازم نیست یکسان باشند. برای مثال، در هسته findMinAndMax، نوع ورودی long، نوع عنصر داده انباشتگر MinAndMax، و نوع نتیجه int2 همگی متفاوت هستند.
چه چیزی را نمیتوانید فرض کنید؟
نباید به تعداد موارد داده انباشتگر ایجادشده توسط RenderScript برای راهاندازی هسته معین اتکا کنید. هیچ تضمینی وجود ندارد که دو راهاندازی یک هسته با ورودی(های) یکسان، تعداد یکسانی از موارد داده انباشتگر ایجاد کند.
نباید به ترتیبی که RenderScript توابع مقداردهی اولیه، انباشتگر، و ترکیبکننده را فراخوانی میکند تکیه کنید؛ حتی ممکن است برخیاز آنها را بهصورت موازی فراخوانی کند. هیچ تضمینی وجود ندارد که دو راهاندازی یک هسته با ورودی یکسان از ترتیب یکسانی پیروی کنند. تنها تضمین این است که فقط تابع مقداردهی اولیه میتواند عنصر داده انباشتگر مقداردهینشده را ببیند. برای مثال:
- تضمینی وجود ندارد که همه موارد دادههای انباشتگر قبلاز فراخوانی تابع انباشتگر مقداردهی اولیه شوند، هرچند این تابع فقط برای مورد دادههای انباشتگر مقداردهیشده اولیه فراخوانی خواهد شد.
- هیچ تضمینی برای ترتیبی که «عناصر» ورودی به تابع انباشتگر ارسال میشوند وجود ندارد.
- هیچ تضمینی وجود ندارد که تابع انباشتگر برای همه «عناصر» ورودی قبلاز فراخوانی تابع ترکیبکننده فراخوانی شده باشد.
یکی از پیامدهای این امر این است که هسته findMinAndMax قطعی نیست: اگر ورودی حاوی بیش از یک رخداد از همان مقدار حداقل یا حداکثر باشد، هیچ راهی برای دانستن اینکه هسته کدام رخداد را پیدا خواهد کرد ندارید.
چه چیزی را باید تضمین کنید؟
ازآنجاکه سیستم RenderScript میتواند انتخاب کند که هسته را به روشهای مختلفی اجرا کند، باید از قوانین خاصی پیروی کنید تا مطمئن شوید که هسته شما همانطور که میخواهید رفتار میکند. اگر این قوانین را رعایت نکنید، ممکن است نتایج نادرست، رفتار غیرقطعی، یا خطاهای زمان اجرا دریافت کنید.
قوانین زیر اغلب میگویند که دو مورد داده انباشتگر باید «مقدار یکسانی» داشته باشند. این یعنی چه؟ این به کاری که میخواهید هسته انجام دهد بستگی دارد. برای کاهش ریاضی مانند addint، معمولاً منطقی است که «یکسان» بهمعنای برابری ریاضی باشد. برای جستجوی «انتخاب هرکدام» مانند findMinAndMax («پیدا کردن مکان حداقل و حداکثر مقادیر ورودی») که ممکن است بیش از یک مورد از مقادیر ورودی یکسان وجود داشته باشد، همه مکانهای یک مقدار ورودی معین باید «یکسان» درنظر گرفته شوند. میتوانید هسته مشابهی برای «پیدا کردن مکان چپترین مقادیر ورودی کمینه و بیشینه» بنویسید که در آن (مثلاً) مقدار کمینه در مکان ۱۰۰ بر مقدار کمینه یکسان در مکان ۲۰۰ ترجیح داده میشود؛ برای این هسته، «یکسان» بهمعنای مکان یکسان است، نه صرفاً مقدار یکسان، و توابع انباشتگر و ترکیبکننده باید متفاوت از توابع findMinAndMax باشند.
تابع مقداردهی اولیه باید مقدار هویت ایجاد کند. یعنی، اگرI و A موارد داده انباشتگر باشند که
توسط تابع مقداردهی اولیه مقداردهی شدهاند، و I هرگز به تابع
انباشتگر ارسال نشده باشد (اما A ممکن است ارسال شده باشد)، دراینصورت
مثال: در هسته addint، یک عنصر داده انباشتگر روی صفر مقداردهی اولیه میشود. تابع ترکیبکننده برای این هسته جمع را انجام میدهد؛ صفر مقدار همانی برای جمع است.
مثال: در هسته findMinAndMax،
یک عنصر داده انباشتگر روی INITVAL مقداردهی اولیه میشود.
fMMCombiner(&A, &I)Aرا بدون تغییر میگذارد، زیراIINITVALاست.fMMCombiner(&I, &A)Iرا رویAتنظیم میکند، زیراIINITVALاست.
بنابراین، INITVAL واقعاً مقدار هویت است.
تابع ترکیبکننده باید جابهجایی باشد. یعنی،
اگر A و B موارد داده انباشتگر باشند که
توسط تابع مقداردهی اولیه مقداردهی شدهاند، و ممکن است صفر یا چند بار به تابع انباشتگر
ارسال شده باشند، دراینصورت combinerName(&A, &B) باید
A را روی همان مقداری تنظیم کند
که combinerName(&B, &A)
B را تنظیم میکند.
مثال: در هسته addint، تابع ترکیبکننده دو مقدار عنصر داده انباشتگر را اضافه میکند؛ جمع شرکتپذیر است.
مثال: در هسته findMinAndMax،
fMMCombiner(&A, &B) با
A = minmax(A, B) یکسان است و minmax جابهجاییپذیر است، بنابراین
fMMCombiner نیز جابهجاییپذیر است.
تابع ترکیبکننده باید شرکتپذیر باشد. یعنی،
اگر A، B، و C
مورد دادههای انباشتگر باشند که توسط تابع مقداردهی اولیه مقداردهی شدهاند، و ممکن است صفر بار یا بیشتر به تابع انباشتگر ارسال شده باشند، دراینصورت دو توالی کد زیر باید
A را روی همان مقدار تنظیم کنند:
combinerName(&A, &B); combinerName(&A, &C);
combinerName(&B, &C); combinerName(&A, &B);
مثال: در هسته addint، تابع ترکیبکننده دو مقدار عنصر داده انباشتگر را اضافه میکند:
A = A + B A = A + C // Same as // A = (A + B) + C
B = B + C A = A + B // Same as // A = A + (B + C) // B = B + C
جمع شرکتپذیر است، بنابراین تابع ترکیبکننده نیز شرکتپذیر است.
مثال: در هسته findMinAndMax،
fMMCombiner(&A, &B)
A = minmax(A, B)
A = minmax(A, B) A = minmax(A, C) // Same as // A = minmax(minmax(A, B), C)
B = minmax(B, C) A = minmax(A, B) // Same as // A = minmax(A, minmax(B, C)) // B = minmax(B, C)
minmax شرکتپذیر است، بنابراین fMMCombiner نیز شرکتپذیر است.
تابع انباشتگر و تابع ترکیبکننده باید باهم از قانون پایه
تا کردن پیروی کنند. یعنی اگر A
و B موارد داده انباشتگر باشند، A توسط تابع مقداردهی اولیه مقداردهی شده است و ممکن است صفر یا چند بار به تابع انباشتگر ارسال شده باشد، B مقداردهی اولیه نشده است، و args فهرست آرگومانهای ورودی و آرگومانهای ویژه برای یک فراخوانی خاص به تابع انباشتگر است، سپس دو توالی کد زیر باید A را روی همان مقدار تنظیم کنند:
accumulatorName(&A, args); // statement 1
initializerName(&B); // statement 2 accumulatorName(&B, args); // statement 3 combinerName(&A, &B); // statement 4
مثال: در هسته addint، برای مقدار ورودی V:
- بیانیه ۱ همان
A += Vاست - بیانیه ۲ همان
B = 0است - گزاره ۳ همان
B += Vاست که همانB = Vاست - بیانیه ۴ همان
A += Bاست که همانA += Vاست
عبارتهای ۱ و ۴ مقدار A را یکسان تنظیم میکنند، بنابراین این هسته از قانون تاشدگی پایه پیروی میکند.
مثال: در هسته findMinAndMax، برای مقدار ورودی V در مختصات X:
- بیانیه ۱ همان
A = minmax(A, IndexedVal(V, X))است - بیانیه ۲ همان
B = INITVALاست - بیانیه ۳ با این بیانیه یکسان است
که چون B مقدار اولیه است، با این عبارت یکسان است:B = minmax(B, IndexedVal(V, X))
B = IndexedVal(V, X)
- گزاره ۴ همانند
که برابر است باA = minmax(A, B)
A = minmax(A, IndexedVal(V, X))
عبارتهای ۱ و ۴ مقدار A را یکسان تنظیم میکنند، بنابراین این هسته از قانون تاشدگی پایه پیروی میکند.
فراخوانی هسته کاهش از کد Java
برای هسته کاهش با نام kernelName که در فایل filename.rs تعریف شده است، سه روش در کلاس ScriptC_filename منعکس شده است:
کاتلین
// Function 1 fun reduce_kernelName(ain1: Allocation, …, ainN: Allocation): javaFutureType // Function 2 fun reduce_kernelName(ain1: Allocation, …, ainN: Allocation, sc: Script.LaunchOptions): javaFutureType // Function 3 fun reduce_kernelName(in1: Array<devecSiIn1Type>, …, inN: Array<devecSiInNType>): javaFutureType
جاوا
// Method 1 public javaFutureType reduce_kernelName(Allocation ain1, …, Allocation ainN); // Method 2 public javaFutureType reduce_kernelName(Allocation ain1, …, Allocation ainN, Script.LaunchOptions sc); // Method 3 public javaFutureType reduce_kernelName(devecSiIn1Type[] in1, …, devecSiInNType[] inN);
در اینجا چند نمونه از فراخوانی هسته addint آورده شده است:
کاتلین
val script = ScriptC_example(renderScript) // 1D array // and obtain answer immediately val input1 = intArrayOf(…) val sum1: Int = script.reduce_addint(input1).get() // Method 3 // 2D allocation // and do some additional work before obtaining answer val typeBuilder = Type.Builder(RS, Element.I32(RS)).apply { setX(…) setY(…) } val input2: Allocation = Allocation.createTyped(RS, typeBuilder.create()).also { populateSomehow(it) // fill in input Allocation with data } val result2: ScriptC_example.result_int = script.reduce_addint(input2) // Method 1 doSomeAdditionalWork() // might run at same time as reduction val sum2: Int = result2.get()
جاوا
ScriptC_example script = new ScriptC_example(renderScript); // 1D array // and obtain answer immediately int input1[] = …; int sum1 = script.reduce_addint(input1).get(); // Method 3 // 2D allocation // and do some additional work before obtaining answer Type.Builder typeBuilder = new Type.Builder(RS, Element.I32(RS)); typeBuilder.setX(…); typeBuilder.setY(…); Allocation input2 = createTyped(RS, typeBuilder.create()); populateSomehow(input2); // fill in input Allocation with data ScriptC_example.result_int result2 = script.reduce_addint(input2); // Method 1 doSomeAdditionalWork(); // might run at same time as reduction int sum2 = result2.get();
روش ۱ یک متغیر مستقل ورودی Allocation برای هر متغیر مستقل ورودی در تابع انباشتگر هسته دارد. زمان اجرای RenderScript بررسی میکند تا مطمئن شود که همه «تخصیصهای» ورودی
ابعاد یکسانی دارند و نوع Element هرکدام از
«تخصیصهای» ورودی با آرگومان ورودی مربوطه در نمونه اولیه تابع انباشتگر
مطابقت دارد. اگر هریک از این بررسیها ناموفق باشد، RenderScript استثنایی را ایجاد میکند. هسته
روی هر مختصات در آن ابعاد اجرا میشود.
روش ۲ همانند روش ۱ است، با این تفاوت که روش ۲ یک آرگومان اضافی sc میگیرد که میتواند برای محدود کردن اجرای هسته به زیرمجموعهای از مختصات استفاده شود.
روش ۳ همانند روش ۱ است با این تفاوت که
بهجای ورودیهای Allocation، ورودیهای آرایه Java را میگیرد. این یک راحتی است که
شما را از نوشتن کد برای ایجاد صریح یک تخصیص و کپی کردن دادهها در آن
از یک آرایه جاوا نجات میدهد. بااینحال، استفاده از «روش ۳» بهجای «روش ۱» عملکرد کد را افزایش نمیدهد. برای هر آرایه ورودی، «روش ۳» یک «تخصیص» ۱-بعدی موقت با نوع Element مناسب و setAutoPadding(boolean) فعالشده ایجاد میکند و آرایه را در «تخصیص» کپی میکند، گویی با روش copyFrom() مناسب Allocation انجام شده است. سپس «روش ۱» را فراخوانی میکند و آن «تخصیصهای» موقت را ارسال میکند.
توجه: اگر برنامه شما چندین فراخوانی هسته با آرایه یکسان یا با آرایههای مختلف با ابعاد و نوع عنصر یکسان انجام میدهد، میتوانید با ایجاد، پر کردن، و استفاده مجدد صریح از «تخصیصها» بهجای استفاده از «روش ۳»، عملکرد را بهبود ببخشید.
javaFutureType،
نوع پاسخ روشهای کاهش منعکسشده، یک کلاس تودرتوی ایستا منعکسشده
در کلاس ScriptC_filename
است. این کلاس نشاندهنده نتیجه آینده اجرای هسته کاهش است. برای دریافت نتیجه واقعی اجرا،
روش get() آن کلاس را فراخوانی کنید که مقداری
از نوع javaResultType را برمیگرداند. get() همگام است.
کاتلین
class ScriptC_filename(rs: RenderScript) : ScriptC(…) { object javaFutureType { fun get(): javaResultType { … } } }
جاوا
public class ScriptC_filename extends ScriptC { public static class javaFutureType { public javaResultType get() { … } } }
javaResultType از resultType تابع outconverter تعیین میشود. مگر اینکه resultType یک نوع بدون علامت (اسکالر، بردار، یا آرایه) باشد، javaResultType مستقیماً نوع Java مربوطه است. اگر resultType نوع بدون علامت باشد و نوع علامتدار Java بزرگتری وجود داشته باشد، javaResultType همان نوع علامتدار Java بزرگتر است؛ درغیراینصورت، مستقیماً نوع Java متناظر است. برای مثال:
- اگر resultType
int،int2، یاint[15]باشد، javaResultTypeint،Int2، یاint[]است. همه مقادیر resultType را میتوان با javaResultType نشان داد. - اگر resultType برابر با
uint،uint2، یاuint[15]باشد، javaResultType برابر باlong،Long2، یاlong[]خواهد بود. همه مقادیر resultType را میتوان با javaResultType نشان داد. - اگر resultType برابر با
ulong،ulong2، یاulong[15]باشد، javaResultType برابر باlong،Long2، یاlong[]است. برخیاز مقادیر resultType را نمیتوان با javaResultType نشان داد.
javaFutureType نوع نتیجه آتی مربوط به resultType تابع outconverter است.
- اگر resultType از نوع آرایه نباشد، javaFutureType
result_resultTypeاست. - اگر resultType آرایهای با طول Count با اعضایی از نوع memberType باشد،
javaFutureType برابر با
resultArrayCount_memberTypeاست.
برای مثال:
کاتلین
class ScriptC_filename(rs: RenderScript) : ScriptC(…) { // for kernels with int result object result_int { fun get(): Int = … } // for kernels with int[10] result object resultArray10_int { fun get(): IntArray = … } // for kernels with int2 result // note that the Kotlin type name "Int2" is not the same as the script type name "int2" object result_int2 { fun get(): Int2 = … } // for kernels with int2[10] result // note that the Kotlin type name "Int2" is not the same as the script type name "int2" object resultArray10_int2 { fun get(): Array<Int2> = … } // for kernels with uint result // note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint" object result_uint { fun get(): Long = … } // for kernels with uint[10] result // note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint" object resultArray10_uint { fun get(): LongArray = … } // for kernels with uint2 result // note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2" object result_uint2 { fun get(): Long2 = … } // for kernels with uint2[10] result // note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2" object resultArray10_uint2 { fun get(): Array<Long2> = … } }
جاوا
public class ScriptC_filename extends ScriptC { // for kernels with int result public static class result_int { public int get() { … } } // for kernels with int[10] result public static class resultArray10_int { public int[] get() { … } } // for kernels with int2 result // note that the Java type name "Int2" is not the same as the script type name "int2" public static class result_int2 { public Int2 get() { … } } // for kernels with int2[10] result // note that the Java type name "Int2" is not the same as the script type name "int2" public static class resultArray10_int2 { public Int2[] get() { … } } // for kernels with uint result // note that the Java type "long" is a wider signed type than the unsigned script type "uint" public static class result_uint { public long get() { … } } // for kernels with uint[10] result // note that the Java type "long" is a wider signed type than the unsigned script type "uint" public static class resultArray10_uint { public long[] get() { … } } // for kernels with uint2 result // note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2" public static class result_uint2 { public Long2 get() { … } } // for kernels with uint2[10] result // note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2" public static class resultArray10_uint2 { public Long2[] get() { … } } }
اگر javaResultType نوع شیء (ازجمله نوع آرایه) باشد، هر فراخوانی
به javaFutureType.get() در همان نمونه، همان شیء را
برمیگرداند.
اگر javaResultType نتواند همه مقادیر نوع resultType را نشان دهد، و
هسته کاهش مقدار غیرقابلنمایشی تولید کند،
javaFutureType.get() استثنایی را ایجاد میکند.
روش ۳ و devecSiInXType
devecSiInXType نوع Java است که با inXType شناسه مربوطه تابع انباشتگر مطابقت دارد. مگر اینکه inXType یک نوع بدون علامت یا نوع برداری باشد، devecSiInXType نوع Java مستقیماً متناظر است. اگر inXType نوع اسکالر بدون علامت باشد، devecSiInXType نوع Java است که مستقیماً با نوع اسکالر علامتدار با همان اندازه مطابقت دارد. اگر inXType نوع بردار علامتدار باشد، devecSiInXType نوع Java است که مستقیماً با نوع مؤلفه بردار مطابقت دارد. اگر inXType نوع بردار بدون علامت باشد، devecSiInXType نوع Java است که مستقیماً با نوع اسکالر علامتدار با همان اندازه نوع مؤلفه بردار مطابقت دارد. برای مثال:
- اگر inXType برابر با
intباشد، devecSiInXType برابر باintاست. - اگر inXType برابر با
int2باشد، devecSiInXType برابر باintاست. آرایه نمایشی مسطح است: تعداد عناصر اسکالر آن دو برابر تعداد عناصر برداری ۲ مؤلفهای در «تخصیص» است. این همان روشی است کهcopyFrom()روشAllocationکار میکند. - اگر inXType برابر با
uintباشد، deviceSiInXType برابر باintاست. مقدار علامتدار در آرایه Java بهعنوان مقدار بدون علامت همان الگوی بیتی در «تخصیص» تفسیر میشود. این همان روشی است که روشهایcopyFrom()Allocationکار میکنند. - اگر inXType برابر با
uint2باشد، deviceSiInXType برابر باintاست. این ترکیبی از روشی است کهint2وuintمدیریت میشوند: آرایه یک نمایش مسطح است و مقادیر علامتدار آرایه Java بهعنوان مقادیر عنصر بدون علامت RenderScript تفسیر میشوند.
توجه داشته باشید که برای روش ۳، انواع ورودی متفاوت از انواع نتیجه مدیریت میشوند:
- ورودی برداری دستورگان در سمت Java مسطح میشود، درحالیکه نتیجه برداری دستورگان مسطح نمیشود.
- ورودی بدون علامت اسکریپت بهعنوان ورودی علامتدار با همان اندازه در طرف Java نشان داده میشود، درحالیکه نتیجه بدون علامت اسکریپت بهعنوان نوع علامتدار گستردهشده در طرف Java نشان داده میشود (بهجز در مورد
ulong).
نمونههای بیشتر از هستههای کاهش
#pragma rs reduce(dotProduct) \ accumulator(dotProductAccum) combiner(dotProductSum) // Note: No initializer function -- therefore, // each accumulator data item is implicitly initialized to 0.0f. static void dotProductAccum(float *accum, float in1, float in2) { *accum += in1*in2; } // combiner function static void dotProductSum(float *accum, const float *val) { *accum += *val; }
// Find a zero Element in a 2D allocation; return (-1, -1) if none #pragma rs reduce(fz2) \ initializer(fz2Init) \ accumulator(fz2Accum) combiner(fz2Combine) static void fz2Init(int2 *accum) { accum->x = accum->y = -1; } static void fz2Accum(int2 *accum, int inVal, int x /* special arg */, int y /* special arg */) { if (inVal==0) { accum->x = x; accum->y = y; } } static void fz2Combine(int2 *accum, const int2 *accum2) { if (accum2->x >= 0) *accum = *accum2; }
// Note that this kernel returns an array to Java #pragma rs reduce(histogram) \ accumulator(hsgAccum) combiner(hsgCombine) #define BUCKETS 256 typedef uint32_t Histogram[BUCKETS]; // Note: No initializer function -- // therefore, each bucket is implicitly initialized to 0. static void hsgAccum(Histogram *h, uchar in) { ++(*h)[in]; } static void hsgCombine(Histogram *accum, const Histogram *addend) { for (int i = 0; i < BUCKETS; ++i) (*accum)[i] += (*addend)[i]; } // Determines the mode (most frequently occurring value), and returns // the value and the frequency. // // If multiple values have the same highest frequency, returns the lowest // of those values. // // Shares functions with the histogram reduction kernel. #pragma rs reduce(mode) \ accumulator(hsgAccum) combiner(hsgCombine) \ outconverter(modeOutConvert) static void modeOutConvert(int2 *result, const Histogram *h) { uint32_t mode = 0; for (int i = 1; i < BUCKETS; ++i) if ((*h)[i] > (*h)[mode]) mode = i; result->x = mode; result->y = (*h)[mode]; }
نمونههای کد اضافی
نمونههای BasicRenderScript، RenderScriptIntrinsic، و Hello Compute استفاده از میاناهای برنامهسازی کاربردی پوششدادهشده در این صفحه را بیشتر نشان میدهند.