手撸 Numpy 的 Slice 与 Pad 算法(C语言描述)

不定维数组的 Slice 与 Padding 算法

Numpy 包里面的数组是一种不定维数组,使用它的时候,我在思考它的 Slice 与 Pad 算法是如何。因为在不定维的情况下 Slice 与 Pad 的算法是比较难实现,经过不断的尝试,最终把算法实现,在此分享记录。

00 Slice 与 Padding

首先,解析一下 Slice 与 Padding 是什么。Slice 意思是分片,在数组上的操作便是截取一部分。而 Padding**相反,意为扩充数组。

可以想象,在一维、二维甚至三维的数组,Slice 与 Padding 都可以很容易实现。Slice 于 Padding 本质是对数组中的元素进行定位,确定要访问的元素的边界与数量。若维数是不确定,那么 Slice 与 Padding 起来就有那么一点麻烦。因为不定数组的维数是不确定的,在做slice 或者 padding 的时候无法通过下标去做数据读取。这就需要通过复杂寻址计算。

01 内存只有一维

在访问多维数组的时候,通常使用数组下标会比较人性化。例如访问二维数组第一行,第三个单元,那么我们可以使用 arr[0][2] 来访问。

其实无论数组表现为多少维,在内存中存储的只有一维。每个数据按照顺序一个个地存放在内存中。通过数组首地址,结合数组下标便可计算得到准确数据的地址,从而实现数据存储。举个例子,一个 5 x 3 的数组,arr[5][3]。这个数组怎么理解他的结构呢?刚刚学编程的人眼里是这样子:
图片

但它其实是长着个样子:一维内存
任何时候数组都只有一维。

那么,在我们要访问元素的时候如何准确的地位到相关的元素呢。当然是通过计算数组的下标得到地址。例如上面的定义的数组arr[5][3], 在访问数组[2][1]的元素时,他的计算公式是:
p o s = a r r + 2 ⋆ 3 + 1 pos = arr + 2 \star 3 + 1 pos=arr+23+1
其中 arr 是数组的首地址,2 是第 2 行,为什么要乘以 3 呢,因为每行有 3个元素,最后加1就是最后寻址到第 2 行后,第 1 个元素的偏移量。可以想象数组 arr[5][3],是由 5 个大盒子,然后装着 3 个小盒子组成。那么 [2][1] b便是要找第 2 个大盒子中的第 1 个小盒子。

02 Slice 与 Padding 算法实现

了解数组的寻址方式后, 然后讨论Slice 与 Padding 的算法实现。先来说说 Slice。实现 Slice 功能的根本是要精确数据定位,因为只有精确地定位到元素,才能准确地将数据截取。上一节说到,多维数组可以比作箱子嵌套,那么也就可以将其比作树型结构,最后一维是叶子节点,以上都是父节点,那么对于元素的定位便可以用递归的方式。

首先考虑最简单的情况,便是一维数组的 Slice, 要截取一维数组中的某一段数据,需要掌握两个条件,1、是数组的首地址,2、是截取数据的偏移量。如图:
在这里插入图片描述
于是 在对多维数组进行 Slice 的时候,1、是要找到每个维度入口的地址。2、是要确定每个维度操作的偏移量

举例,现有一个 6 * 5 * 4 *3 的整形数组,然后我要进行截取的是 “1:3, 2:4, 2:3, 1:2”, 这里我们约定使用左闭右开 “[ )" 的方式约定截取范围。现在面对的难点:1、此数组不定维的数组,于是我们不能强求下标的方式去寻址元素。2、在物理上此数组上的元素就是在一维的空间里面排列,所以需要将元素的坐标定位信息从 1 维转换到 X 维,或者将 X 维的坐标转换到 1 维。

再来看看我们的 Slice 参数:“1:3, 2:4, 2:3, 1:2”, 在第一维度,需要截取 1 ≤ x < 3 1 \le x \lt 3 1x<3个单位,这个怎么定位呢,首先看数组从 0 开始,截取从 1 开始,那么我么第一个维度就要 1 个偏移量。那么第一个维度的一个单位是多少呢?

按照上一节的计算方法一个单位的大小应该是:

Uszie =  5 * 4 * 3 * sizeof(float);

那么偏移一个单位就是 1 * Usize。哪个位置开始偏移呢?便是从数组首地址开始偏移。假设数组首地址是 Base_arr。

那么第一个维度截取的开始地址便是:


addr = Base_arr + 1 * Usize

那么同理我们再看第二个维度的截取地址,在第二个维度,需要截取 2 ≤ x < 4 2\le x \lt4 2x<4 它从第三个单位开始。在第二个维度每个单元大小便是:

Usize = 4 * 3 * sizeof(float);

那么偏移两个单位的就是 2 * Usize。那么重点来了,从哪个位置开始偏移呢?应该从上一级维度,所截取每个单元的开始地址。就是在第一维度中截取第 1, 第 2 个单元(1:3),再从这第1,第2个单元中截取第二维度的第2,第3个单元(2:4),然后再从第2维度的第2,第3个单元找截取第三个维度的第2个单元(2:3), 最后从第三个维度中截取第四维度的第1单元(1:2)。也就是本维度截取基地址是上一维度截取单元的首地址。

03 看代码

首先假定一个不定维的数组设计如下:

typedef struct  {
    int  axisn;
    int* dimens;
    float* elemes;
} XArray_t;

其中 axisn 存储不定数组的维度数量, dimens 指针指向一个整型数组,此数组存储不定数组的维度信息,由于是不定维数组,所以维度信息数组的长度也是不定的。elems 指向一个内存池,池中便是存储数组的真实数据。

设计一下它初始化函数如下:

XArray_t* create_array(int axisn, ...)
{
  va_list valist;
  va_start(valist, axisn);
  
  XArray_t* arr = malloc(sizeof(XArray_t):

  arr->axisn = axisn;
  
  int elem_number = 1;
  // 申请存放维度信息的数组
  arr->dimens= malloc(axisn * sizeof(int));
  // 从不定参数中获取维度信息
  for (int i=0; i<axisn; ++i){
      (int*)(arr->handle[0]))[i] = va_arg(valist, int);
      elem_number *= (int*)(arr->handle[0]))[i] ;
  }
  
  // 假设它是一个浮点型的数组
  arr->elems = malloc( elem_number * sizeof (float));
  
  return arr;
}


// 那么生成一个以上的数组可以如下调用:
XArray_t* arr = create_array(4, 6, 5, 4,3);

设计好模型后,我们来实现 Slice 的算法:


// 应该是多个循环嵌套,直到最后一个维度为止:
for (...) {
  for (...) {
    for (...) {
        ....
        ...
        ...
    }
  }

}

不对,不定维数组,不知道应该嵌套多少层的 for 循环。所以搞错了,

重来 !!!

设计好模型后我们来设计 Slice 的算法。

XArray_t* array_slice(XArray_t* arr, ...)
{
  
  XArray_t* slice_arr = malloc(sizeof(XArray_t));
  
  va_list valist;
  va_start(valist, arr);
  
  // 每个维度应该有两个截取信息,所以截取信息改为维度数量的两倍。
  int slice_info[arr->axisn * 2];
  
  
  for (int i=0; i<arr->axisn * 2; ++i) {
    slice_info[i] = va_arg(valist, int);
  }
  
  // 根据 slice_info 测算出新数组的维度
  slice_arr->axisn = arr->axisn;
  slice_arr->dimens = malloc (slice_arr->axisn * sizeof(int);
  int elems_size = 1;
  
  // 从截取的信息中获取新数组的维度信息
  for (int j=0; j<slice_arr->axisn; j+=2){
    int diff = slice_info[j+1] - slice_info[j];
    slice_arr->dimens[j] = diff;
    elemes_size *= diff;
    
  }
  
  // 至此新的数组建立完成
  slice_arr->elems = malloc (elems_size * sizeof(float));
  
  // 核心函数,把要截取额数据从 arr copy 到 slice_arr 中。
  void* dest = slice_arr->elems;
  
  do_slicing( &dest, arr->elems, slice_info, 0);
  
  return slice_arr;

}

核心函数 do_slicing 实现如下:

void do_slice(void** dest, void* base, int* slice_info, int curr_dimen)
{
   // 
   int axisn = src->axisn;
   // 计算当前维度下,每一个单元的大小
   int Usize = 1;
   for (int i = curr_dimen+1; i < axisn; ++i)
   {
     Usize *= src->dimens[i];
   }
   Usize *= sizeof(float);
   
   int slice_start = slice_info[curr_dimen];
   int slice_end   = slice_info[curr_dimen+1];
   int diff        = slice_end - slice_start;
   
   if (curr_dimens == axisn - 1) {
      // 最后一个维度,是存放真是元素的。
      // 当来到最后一个维度的时候,便把 src 元素拷贝到 dest 
      // 指向的内存,并且该 dest 内存指针也要往前递进,
      // 配合下一次的数据拷贝。
      offset = (char*)base;
      offset += slice_start * Usize;
      memcpy(*dest, offset, diff * sizeof(float));
      *dest += diff * sizeof(float);
   } else {
     // 如果发现当前维度并不是 最后一个维度
     // 那么便计算出当前维度被截取的单位的地址,
     // 用其地址作为下一维度的基础地址。
     char* offset;
     for (int j=slice_start; j <slice_end; ++j) {
         offset = (char*)base;
         offset += j * Usize;
         do_slice(dest, offset, slice_info, curr_dimen + 1);
     }
   }
}

以上实现的逻辑是递归算法,主要是直到每个维度需要截取的单元的首地址,用于作为下一个维度截取时参考坐标。Padding 实现和 Slice 类似,只不过在填充新数组的数据的时候我们必须确定好边界。那些位置是填入 Padding 的值,那些是需要从原数组拷贝。

04 看效果

1 本文slice算法效果:
在这里插入图片描述
2 Numpy 的 Slice:
在这里插入图片描述
3 本文的 Padding 算法在这里插入图片描述
4 Numpy 的 pad 算法
在这里插入图片描述
结果一致, 想要了解更多无聊的算法,欢迎关注,评论、转发、点赞!
源码:https://github.com/zuweie/boring-code/blob/main/src/ultra_array/x_array.c

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值