Ruby 扩展,将大数据流传输到 ruby



我的C库生成了一个非常大的POD结构数组。将其传递给 Ruby 端的最有效方法是什么?在 Ruby 方面,原始值数组对我来说很好。

我当前的解决方案通过单独存储每个元素和字段来工作,并且速度非常慢。分析表明,此功能平均占用了大约 ~15% 的程序时间,甚至不是计算部分。

我已经阅读了Data_Wrap_Struct,但不确定我需要它。如果我将原始void*传递给字符串,然后在 Ruby 端解压缩它,它会更快吗?

struct SPacket
{
    uint32_t field1;
    uint32_t field2;
    uint16_t field3;
    uint8_t field4;
};
VALUE rb_GetAllData(VALUE self) // SLOOOW
{
    size_t count = 0;
    struct SPacket* packets = GetAllData(&count);
    VALUE arr = rb_ary_new2(count);
    for(size_t i = 0; i < count; i++)
    {
        VALUE sub_arr = rb_ary_new2(4);
        rb_ary_store(sub_arr, 0, UINT2NUM(packets[i].field1));
        rb_ary_store(sub_arr, 1, UINT2NUM(packets[i].field2));
        rb_ary_store(sub_arr, 2, UINT2NUM(packets[i].field3));
        rb_ary_store(sub_arr, 3, UINT2NUM(packets[i].field4));
        rb_ary_store(arr, i, sub_arr);
    }
    return arr;
}

您的方法将 C 数组复制到 Ruby 数组中。你可以避免这种情况,我创建了一个 Ruby 集合类,该类使用 Data_Wrap_Struct 包装 C 数组并直接对其执行操作。

Data_Wrap_Struct是一个宏,它采用一个 Ruby 类和一个 C struct(以及可选的几个指向我故意省略的内存管理函数的指针(,并创建该类的实例,该实例具有"附加"struct。在提供此类方法实现的函数中,您可以使用Data_Get_Struct"解包"随后可以在函数中访问的struct

在这种情况下,如下所示:

// declare a variable for the new class
VALUE rb_cSPacketCollection;
// a struct that will be wrapped by the class
struct SPacketDataStruct {
    struct SPacket * data;
    int count;
};
VALUE rb_GetAllData() {
    struct SPacketDataStruct* wrapper = malloc(sizeof (struct SPacketCollectionWrapper));
    wrapper->data = GetAllData(&wrapper->count);
    return Data_Wrap_Struct(rb_cSPacketCollection, 0, 0, wrapper);
}

Init_whatever()方法中,您需要创建类:

rb_cSPacketCollection = rb_define_class("SPacketCollection", rb_cObject);

仅此一项用处不大,您需要在这个新类上定义一些方法。例如,您可以创建一个[]方法来允许访问各个SPacket

VALUE SPacketCollection_get(VALUE self, VALUE index) {
    // unwrap the struct
    struct SPacketDataStruct* wrapper;
    Data_Get_Struct(self, struct SPacketDataStruct, wrapper);
    int i = NUM2INT(index);
    // bounds check
    if (i >= wrapper->count) {
        rb_raise(rb_eIndexError, "Index out of bounds");
    }
    // just return an array in this example
    VALUE arr = rb_ary_new2(4);
    rb_ary_store(arr, 0, UINT2NUM(wrapper->data[i].field1));
    rb_ary_store(arr, 1, UINT2NUM(wrapper->data[i].field2));
    rb_ary_store(arr, 2, UINT2NUM(wrapper->data[i].field3));
    rb_ary_store(arr, 3, UINT2NUM(wrapper->data[i].field4));
    return arr;
}

然后在Init_方法中,在创建类后定义方法:

rb_define_method(rb_cSPacketCollection, "[]", SPacketCollection_get, 1);

请注意Data_Get_Struct是一个宏,用法有点奇怪,因为它不返回未包装的struct

由于您在此阶段已经开始使用 Data_Wrap_Struct,因此您可以更进一步创建一个新类,该类包装单个 SPacket 结构并直接对其进行操作:

// declare a variable for the new class
VALUE rb_cSPacket;
//and a function to get a field value
// you'll need to create more methods to access the other fields
// (and possibly to set them)
VALUE SPacket_field1(VALUE self) {
    struct SPacket* packet;
    Data_Get_Struct(self, struct SPacket, packet);
    return UINT2NUM(packet->field1);
}

Init_函数中,创建它并定义方法:

rb_cSPacket = rb_define_class("SPacket", rb_cObject);
rb_define_method(rb_cSPacket, "field1", SPacket_field1, 0);

这可能需要一些工作来为田地创建所有 getter 和 setter,这取决于您如何使用它。像 ffi 这样的东西可以在这里提供帮助,但我不知道 ffi 将如何处理集合类——它可能值得研究。

现在更改您的 [] 函数以返回一个实例,如果这个新类:

VALUE SPacketCollection_get(VALUE self, VALUE index) {
    //unwrap the struct
    struct SPacketDataStruct* wrapper;
    Data_Get_Struct(self, struct SPacketDataStruct, wrapper);
    int i = NUM2INT(index);
    //bounds check
    if (i >= wrapper->count) {
        rb_raise(rb_eIndexError, "Index out of bounds");
    }
    //create an instance of the new class, and wrap it around the 
    //struct in the array
    struct SPacket* packet = &wrapper->data[i];
    return Data_Wrap_Struct(rb_cSPacket, 0, 0, packet);
}

有了这个,你现在可以在 Ruby 中做这样的事情:

c = get_all_data # in my testing I just made this a global method
c[2].field1 # returns the value of field1 of the third SPacket in the array

可能值得在集合类上创建一个 each 方法,然后您可以包含 Enumerable 模块并提供大量方法:

VALUE SPacketCollection_each(VALUE self) {
    //unwrap the struct as before
    struct SPacketDataStruct* wrapper;
    Data_Get_Struct(self, struct SPacketDataStruct, wrapper);
    int i;
    for(i = 0; i < wrapper->count; i++) {
        //create a new instance if the SPacket class
        // wrapping this entry
        struct SPacket* packet = &wrapper->data[i];
        rb_yield(Data_Wrap_Struct(rb_cSPacket, 0, 0, packet));
    }
    return self;
}

Init_whatever

rb_define_method(rb_cSPacketCollection, "each", SPacketCollection_each, 0);
rb_include_module(rb_cSPacketCollection, rb_mEnumerable);

在这个例子中,我没有关心对象标识和内存管理之类的事情。由于所有内容都由同一数组支持,因此您可能有多个对象共享相同的数据,您必须考虑这是否适合您使用。你可能已经注意到我已经malloc了,但没有free d。您需要确定谁"拥有"数据数组,并确保不会引入任何内存泄漏。您可以将函数传递给Data_Wrap_Struct,该函数将在对象被垃圾回收以释放内存时调用。

如果你还没有看过,Pickaxe 书中有一章关于 C 扩展的好章节,现在可以在线获得。

最新更新