আমরা কি ক্রুবির জন্য একটি দ্রুত এফএফআই পেতে পারি? হ্যাঁ।
আমরা কি ক্রুবির জন্য একটি দ্রুত এফএফআই পেতে পারি?
আমি রুবিতে প্রোগ্রামিং পছন্দ করি এবং আমি যতটা সম্ভব রুবি লেখার পক্ষে পরামর্শ দিয়েছি। তবে কখনও কখনও আপনাকে সত্যিই সত্যিই নেটিভ কোডে কল করতে হবে। এমনকি এই ক্ষেত্রেও আমি মানুষকে উত্সাহিত করি যতটা সম্ভব রুবি লিখুনবিশেষত কারণ ওয়াইজিট রুবি কোডটি অনুকূল করতে পারে তবে সি কোড নয়।
এর যৌক্তিক চরম দিকে নিয়ে যাওয়া, এই নির্দেশিকাটির অর্থ হ’ল আপনি যদি কোনও নেটিভ লাইব্রেরিতে কল করতে চান তবে আপনার খুব সীমাবদ্ধ এপিআই সহ একটি নেটিভ এক্সটেনশন লিখতে হবে যেখানে রুবিতে বেশিরভাগ কাজ করা হয়। যে কোনও নেটিভ কোড ফাংশনের চারপাশে খুব পাতলা মোড়ক হবে আমরা আসলে কল করতে চাই এটি কেবল রুবি প্রকারগুলিকে নেটিভ ফাংশন দ্বারা প্রয়োজনীয় প্রকারগুলিতে রূপান্তর করে।
অবশ্যই এই জাতীয় সরল এপিআই এফএফআইয়ের মতো লাইব্রেরির সাথে কাজ করার জন্য উপযুক্ত হবে।
এখন, সাধারণত আমি এফএফআই থেকে পরিষ্কারভাবে চালিত করি এবং সত্য কথা বলতে কেবল এটিই হ’ল এটি দেশীয় এক্সটেনশনের মতো একই কর্মক্ষমতা সরবরাহ করে না।
আমি কী বলতে চাইছি তা আরও ভালভাবে বুঝতে একটি খুব সাধারণ উদাহরণ বেঞ্চমার্কটি একবার দেখে নেওয়া যাক। এই মানদণ্ডে, আমরা এটি মোড়ানো যাচ্ছি strlen
সি এফএফআই সহ ফাংশন। আমরা এফএফআই বাস্তবায়নকে একটি সি এক্সটেনশনের সাথে তুলনা করব যা একই কাজ করে (ব্যবহার করে strlen
রুবি রত্ন যা আপনার সত্যই এই পোস্টের জন্য লিখেছিল)। আমরা পরোক্ষভাবে কল করার সাথে একটি তুলনাও অন্তর্ভুক্ত করব String#bytesize
পাশাপাশি সরাসরি কল String#bytesize
।
require "ffi"
require "strlen"
require "benchmark/ips"
module A
extend FFI::Library
ffi_lib 'c'
attach_function :strlen, (:string), :int
end
module B
def self.strlen(x)
x.bytesize
end
end
str = "foo"
Benchmark.ips do |x|
x.report("strlen-ffi") A.strlen(str)
x.report("strlen-ruby") B.strlen(str)
x.report("strlen-cext") Strlen.strlen(str)
x.report("ruby-direct") str.bytesize
x.compare!
end
এখানে বেঞ্চমার্কের আউটপুট রয়েছে:
ruby 3.5.0dev (2025-02-11T16:42:26Z master 4ac75f6f64) +PRISM (arm64-darwin24)
Warming up --------------------------------------
strlen-ffi 1.557M i/100ms
strlen-ruby 2.875M i/100ms
strlen-cext 3.047M i/100ms
ruby-direct 4.048M i/100ms
Calculating -------------------------------------
strlen-ffi 15.682M (± 0.5%) i/s (63.77 ns/i) - 79.398M in 5.063141s
strlen-ruby 28.697M (± 0.3%) i/s (34.85 ns/i) - 143.747M in 5.009135s
strlen-cext 30.661M (± 0.8%) i/s (32.61 ns/i) - 155.406M in 5.068838s
ruby-direct 39.879M (± 0.6%) i/s (25.08 ns/i) - 202.412M in 5.075857s
Comparison:
ruby-direct: 39878845.7 i/s
strlen-cext: 30661398.4 i/s - 1.30x slower
strlen-ruby: 28697184.3 i/s - 1.39x slower
strlen-ffi: 15681971.0 i/s - 2.54x slower
প্রথমত, সরাসরি কল করা String#bytesize
দ্রুততম, এবং আমরা এটিকে আমাদের বেসলাইন হিসাবে ভাবতে পারি। আমরা যে কোনও নির্বাহী যুক্ত করব তা অগত্যা আরও বেশি ওভারহেড যুক্ত করবে এবং আমরা সম্ভবত এই সংখ্যাটি “বীট” করতে পারি না। কলিং strlen
সি এক্সটেনশন মাধ্যমে দ্বিতীয় দ্রুততম, পরে পরোক্ষভাবে কল করা হয় String#bytesize
এবং অবশেষে এফএফআই বাস্তবায়ন ধীরতম।
এই বেঞ্চমার্ক ফলাফলগুলি আমাদের কয়েকটি আকর্ষণীয় জিনিস শিখিয়ে দিতে পারে।
প্রথমত, “রুবি-ডাইরেক্ট” বেঞ্চমার্ক এবং “স্ট্র্লেন-রুবি” বেঞ্চমার্কের মধ্যে পার্থক্য দেখায় যে স্ট্যাক ফ্রেমগুলি পপিং এবং পপিংয়ে অবশ্যই ওভারহেড রয়েছে। এই ওভারহেডটি মুছে ফেলা এমন একটি জিনিস যা ইজিটের মতো জেআইটি সংকলকগুলিতে বিশেষজ্ঞ।
দ্বিতীয়ত, “স্ট্র্লেন-সেক্সট” বেঞ্চমার্ক এবং “স্ট্র্লেন-এফএফআই” বেঞ্চমার্কের মধ্যে পার্থক্য দেখায় যে এফএফআইয়ের মাধ্যমে একটি নেটিভ ফাংশন কল করার সময় উল্লেখযোগ্য ওভারহেড রয়েছে। সি এক্সটেনশন কল করা সরাসরি কল করার চেয়ে ধীর String#bytesize
কিন্তু কলিং strlen
এফএফআই যোগ করে আরও বেশি সি এক্সটেনশনের চেয়ে ওভারহেড।
অন্য কথায়, যদি রুবি আপনার প্রয়োজনীয় কিছু করার জন্য কোনও পদ্ধতি সরবরাহ করে তবে কেবল রুবি সরবরাহ করে এমন পদ্ধতিটি ব্যবহার করুন। তবে আপনার যদি কোনও বিদেশী ফাংশন কল করতে হয় তবে একটি ছোট সি এক্সটেনশন মোড়কের সাধারণত এফএফআই মোড়কের চেয়ে ওভারহেড কম থাকে।
আমি এফএফআই এড়াতে পারি নি কারণ আমি মনে করি এটি অভ্যন্তরীণভাবে খারাপ একটি সি এক্সটেনশনের চেয়ে। বরং, এফএফআই ট্যাক্স প্রদান করা কেবল একটি বাস্তবতা যা আমি এড়াতে চেষ্টা করেছি।
আমরা কি বাস্তবতা পরিবর্তন করতে পারি?
কয়েক বছর আগে ক্রিস সিটন আমাকে এমন একটি ধারণা দিয়েছে যা তখন থেকেই আমার মাথায় ঘুরছে। তৃতীয় পক্ষের লাইব্রেরিতে কল করার পরিবর্তে, আমরা কি কেবল বাহ্যিক ফাংশনটি কল করার জন্য প্রয়োজনীয় কোডটি জিট করতে পারি?
এফএফআই মোড়কের উদাহরণটি একবার দেখে নেওয়া যাক:
module A
extend FFI::Library
ffi_lib 'c'
attach_function :strlen, (:string), :int
end
কল attach_function
আমাদের যে ফাংশনটি কল করতে হবে তার নাম আমাদের জানায় (strlen
) পাশাপাশি প্যারামিটারের ধরণগুলি (একটি স্ট্রিং) এবং রিটার্ন টাইপ (একটি আইএনটি)। যেহেতু আমরা এই প্রকারগুলি জানি যখন আমরা মোড়ক ফাংশনটি সংজ্ঞায়িত করছি, তাই আমরা রুবি প্রকারগুলি মোড়ানো এবং মোড়ক দেওয়ার জন্য প্রয়োজনীয় মেশিন কোড তৈরি করতে পারি, পাশাপাশি বিদেশী ফাংশনে কল করতে পারি।
কয়েক বছর ধরে আমি এটি করার উপায়ের জন্য পরিকল্পনা করছি এবং আমি মনে করি তারকারা অবশেষে এই বছরের শেষের দিকে রুবি 3.5 প্রকাশের সাথে একত্রিত হবে।
এই স্বপ্নটি ঘটানোর জন্য, আমাদের একত্রিত হওয়ার জন্য কয়েকটি জিনিস প্রয়োজন।
প্রথমত, আমাদের মেশিন কোড উত্পন্ন করার একটি উপায় প্রয়োজন। এই কারণেই আমি লিখেছি Aarch64 রত্ন পাশাপাশি ফিস্ক রত্ন যা যথাক্রমে এআরএম 64 এবং x86_64 মেশিন কোড তৈরি করতে পারে।
দ্বিতীয়ত, আমাদের এক্সিকিউটেবল মেমরি বরাদ্দ করার একটি উপায় প্রয়োজন যাতে আমরা আসলে করতে পারি কার্যকর করা মেশিন কোড। একত্রিত মেশিন কোডটি যথেষ্ট ভাল নয়, আমাদের সেই মেশিন কোডটি স্মৃতিতে রাখতে হবে যা “এক্সিকিউটেবল” হিসাবে চিহ্নিত। এজন্য আমি সৃজনশীল নাম লিখেছি জিতবফার রত্ন।
এই ইউটিলিটিগুলির সাথে, আমাদের এক্সিকিউটেবল মেশিন কোড তৈরি করার একটি উপায় রয়েছে। দুর্ভাগ্যক্রমে, আমাদের কাটিয়ে উঠতে আরও একটি বাধা রয়েছে এবং এটি চেষ্টা করছে মেশিন কোডে লাফিয়ে রুবি পান।
কেবল এক্সিকিউটেবল মেশিন কোড তৈরি করা যথেষ্ট ভাল নয়। মিসফিটসের যে কোনও র্যাগ-ট্যাগ দল এটি করতে পারে। আমাদের সেই মেশিন কোডে ঝাঁপিয়ে পড়ার জন্য রুবিও পেতে হবে যাতে আমরা পারি এফএফআই ওভারহেড এড়িয়ে যান।
Rjit verying
যাঁরা জানেন না তাদের জন্য, আরজিট রুবির জন্য একটি জেআইটি সংকলক যা নিজেই রুবিতে লেখা এবং এটি রুবির সাথে জাহাজও করে। এটি অভ্যন্তরীণ কাঠামো ইজিটের সাথে বেশ মিল, তবে এটি উত্পাদন ব্যবহারের উদ্দেশ্যে নয়, এ কারণেই বেশিরভাগ লোকেরা সম্ভবত ইজিটের কথা শুনেছেন তবে আরজিট নয়।
আরজিটের লেখক কোকুবুন সম্প্রতি একটি বৈশিষ্ট্য অনুরোধ দায়ের করেছেন রত্ন হিসাবে আরজিট বের করুন। এই নিষ্কাশন দ্বারা সরবরাহিত প্রধান বৈশিষ্ট্যটি হ’ল লোকেরা তৃতীয় পক্ষের রত্ন হিসাবে রুবির জন্য জেআইটি সংকলকগুলি আরও সহজেই লিখতে সক্ষম হবে। প্রস্তাবিত বৈশিষ্ট্যটি 2 টি গুরুত্বপূর্ণ কাজ করে।
প্রথমত, এটি রত্ন হিসাবে আরজিটকে নিষ্কাশন করে। আরজিট অনুরূপ একটি প্রক্রিয়া ব্যবহার করে মরিচা থেকে বিন্দজেনএটা যেখানে রুবি ডেটা স্ট্রাকচার উত্পন্ন করে এই মানচিত্রটি রুবির সমস্ত অভ্যন্তরীণ প্রকারের (আপনি উত্পন্ন কোডের কয়েকটি দেখতে পারেন এখানে)। এর অর্থ হ’ল তৃতীয় পক্ষের জেআইটি সংকলকগুলি তাদের প্রয়োজনীয় তথ্য পেতে পারে মোড়ানো এবং রুবি ডেটা প্রকারগুলি মোড়ানো।
এটি দ্বিতীয় গুরুত্বপূর্ণ জিনিস যদি একটি থাকে তবে সর্বদা জেআইটি এন্ট্রি ফাংশন পয়েন্টারটি কার্যকর করুন। এটি গুরুত্বপূর্ণ কারণ এর অর্থ হ’ল তৃতীয় পক্ষের জিটের তাদের মেশিন কোডটি নিবন্ধ করার একটি উপায় থাকবে এবং রুবি স্বয়ংক্রিয়ভাবে সেই মেশিন কোডে ঝাঁপিয়ে পড়বে।
এই দুটি টুকরো স্থানে রয়েছে, আমরা একটি খুব ছোট আকারের, একক-উদ্দেশ্যমূলক জেআইটি সংকলক লিখতে পারি যা এফএফআই ইন্টারফেস হিসাবে কাজ করে।
ধারণার প্রমাণ
আমি একটি খুব ছোট তৈরি ধারণার প্রমাণ “fjit” বলা হয়। “এফজিট” “এফএফআই জিত” এর জন্য সংক্ষিপ্ত এবং এটি টিনে যা বলে তা করে। যথা, এটি রানটাইমে মেশিন কোড তৈরি করে যা কোনও বিদেশী ফাংশনকে কল করতে পারে। এই ক্ষেত্রে আমরা এটি কল করতে ব্যবহার করতে যাচ্ছি strlen
ফাংশন।
আমি এই পোস্টে পুরো প্রোগ্রামটি রাখতে যাচ্ছি না কারণ এটি “ছোট” হলেও এটিতে এখনও একটি সম্পূর্ণ জেআইটি সংকলক রয়েছে। গুরুত্বপূর্ণ অংশটি হ’ল বেঞ্চমার্ক:
module A
extend FFI::Library
ffi_lib 'c'
attach_function :strlen, (:string), :int
end
module B
def self.strlen(x)
x.bytesize
end
end
module C
extend FJIT
attach_function :strlen, (:string), :int
end
str = "foo"
Benchmark.ips do |x|
x.report("strlen-ffi") A.strlen(str)
x.report("strlen-ruby") B.strlen(str)
x.report("strlen-cext") Strlen.strlen(str)
x.report("ruby-direct") str.bytesize
x.report("strlen-fjit") C.strlen(str)
x.compare!
end
মডিউল C
এই আপডেট হওয়া বেঞ্চমার্ক একটি ব্যবহার করে FJIT
মডিউল, এবং আপনি দেখতে পাচ্ছেন যে এর ইন্টারফেসটি এফএফআইয়ের সাথে খুব মিল। কখন attach_function
বলা হয়, Fjit রুবি স্ট্রিংটি আনল্যাপ করার জন্য প্রয়োজনীয় মেশিন কোড তৈরি করবে, কল করুন strlen
ফাংশন করুন, এবং একটি রুবি অবজেক্ট হিসাবে স্ট্রিংয়ের দৈর্ঘ্য ফিরিয়ে দিন।
এখানে বেঞ্চমার্কের ফলাফল রয়েছে:
ruby 3.5.0dev (2025-02-11T16:42:26Z master 4ac75f6f64) +RJIT +PRISM (arm64-darwin24)
Warming up --------------------------------------
strlen-ffi 1.558M i/100ms
strlen-ruby 2.953M i/100ms
strlen-cext 2.981M i/100ms
ruby-direct 4.142M i/100ms
strlen-fjit 3.206M i/100ms
Calculating -------------------------------------
strlen-ffi 15.629M (± 0.7%) i/s (63.98 ns/i) - 79.455M in 5.083899s
strlen-ruby 28.851M (± 0.3%) i/s (34.66 ns/i) - 144.704M in 5.015659s
strlen-cext 29.778M (± 2.8%) i/s (33.58 ns/i) - 149.025M in 5.008456s
ruby-direct 41.907M (± 0.8%) i/s (23.86 ns/i) - 211.219M in 5.040449s
strlen-fjit 32.508M (± 0.9%) i/s (30.76 ns/i) - 163.504M in 5.030060s
Comparison:
ruby-direct: 41907248.7 i/s
strlen-fjit: 32507961.2 i/s - 1.29x slower
strlen-cext: 29778234.0 i/s - 1.41x slower
strlen-ruby: 28850712.3 i/s - 1.45x slower
strlen-ffi: 15629443.7 i/s - 2.68x slower
অবশ্যই সরাসরি কল String#bytesize
এখনও দ্রুততম। তবে, এফজেআইটি দ্বারা উত্পাদিত মেশিন কোডটি দ্বিতীয় দ্রুততম। আশ্চর্যজনকভাবে, এটি এর চেয়ে কিছুটা দ্রুত strlen
সি এক্সটেনশন। তবে আরও প্রতিশ্রুতিবদ্ধ যে এটি পরোক্ষ রুবি কলের চেয়ে দ্রুত এবং এফএফআইয়ের মাধ্যমে কল করার চেয়ে 2x এরও বেশি দ্রুত!
উপসংহার
আমি মনে করি এটি অত্যন্ত উত্তেজনাপূর্ণ কারণ এর অর্থ হ’ল আমরা “যতটা সম্ভব রুবি লিখুন” দর্শন বজায় রেখে আমরা সি এক্সটেনশনের চেয়ে একই গতি (বা আরও ভাল) অর্জন করতে পারি। আমি জিগের মতো প্রোগ্রামিং ভাষাগুলিতে খুব alous র্ষা করেছি, যা এফএফআই ব্যবহার না করেই নেটিভ কোড কলিংকে সমর্থন করতে সক্ষম। যদি আমরা এই চলমান সমস্ত অংশগুলি নিষ্পত্তি করতে পারি তবে আমি মনে করি রুবির একই সুবিধা থাকতে পারে।
সতর্কতা
আমি জানি উপসংহারটি শেষ হওয়ার কথা, যেহেতু এটি একটি “উপসংহার”। তবে আমি চাইনি যে লোকেরা ভাল জিনিসগুলিতে পৌঁছানোর আগে বর্তমান সতর্কতাগুলি দিয়ে ঝাঁকুনি দেয়।
প্রথমত, আমি লিখেছি জেআইটি সংকলক ধারণার প্রমাণে এআরএম 64 প্ল্যাটফর্মগুলিতে সীমাবদ্ধ। আমরা যদি এটি “বাস্তবের জন্য” তৈরি করতে চাই তবে আমাদের একটি x86_64 ব্যাকএন্ড যুক্ত করতে হবে। অবশ্যই এটি সম্ভব, এটি কেবল করা দরকার। দ্বিতীয়ত, এটি বর্তমানে সমস্ত প্যারামিটারের ধরণ এবং রিটার্ন প্রকারগুলি পরিচালনা করে না। আমি নিশ্চিত যে আমরা সমস্ত প্যারামিটারের প্রকারকে সমর্থন করতে পারি এবং কাজটি কঠোর হবে না। তৃতীয়ত, এটি কেবলমাত্র ফাংশনগুলি পরিচালনা করে যা একটি একক প্যারামিটার নেয় এবং একটি একক প্যারামিটার দেয়। আবার, আমি মনে করি এটি বাকী সংকলকটি বের করার বিষয় মাত্র। চতুর্থত, আপনাকে সাথে রুবি চালাতে হবে --rjit --rjit-disable
এই মুহুর্তে পতাকা। একবার কোকুবুনের বৈশিষ্ট্য জমি, এটি আর হওয়া উচিত নয়। সর্বশেষে, ধারণার এই প্রমাণটি কেবল এই মুহুর্তে বর্তমান রুবি হেডের সাথে চলে।
হু, আমি এর বিধিনিষেধগুলির মোটামুটি দীর্ঘ তালিকা জানি, তবে এটি গড় EULA এর চেয়ে খাটো, এবং এমন কিছুই যা আমরা কাটিয়ে উঠতে পারি না।
যাইহোক, এটাই শেষ। ভাল দিন কাটুক!