dustpg / BlogFM

Blog for Me
MIT License
155 stars 23 forks source link

自己常用的C/C++小技巧[3] #34

Open dustpg opened 5 years ago

dustpg commented 5 years ago

自己常用的小技巧

这里列出了自己常用的一些c/c++小技巧, 有些会有不足, 可以简单探讨一下.

颜色混合

分类: 小技巧

有一个可能很常用的操作, 或者说函数: 平均数. 通过基础数学我们可以知道(a+b)/2, 如果用c中无符号整型表示:

uint32_t avg(uint32_t a, uint32_t b) {
    return (a+b)/2;
}

一般情况下没问题, 但是在两个数足够大时可能会产生溢出, 由于乘法分配律可知, 我们可以实现为:

uint32_t avg(uint32_t a, uint32_t b) {
    return a/2 + b/2;
}

但是我们操作的是无符号整型, 两个奇数一操作最低位就被忽略了, 所以我们可以利用位操作, 再加上除以2可以化作右移一位, 我们可以实现为:

uint32_t avg(uint32_t a, uint32_t b) {
    return (a>>1) + (b>>1) + (a&b&1);
}

现在我们可以推广至颜色上, 我们现在常用的是R8G8B8X8格式, X表示忽略或者表示不透明度. 如果现在我们规定低到高位分别是RGB, 最高8位忽略, 并假设为0:

#define R8G8B8_MASK_HI ((uint32_t)0x00FEFEFE)
#define R8G8B8_MASK_LO ((uint32_t)0x00010101)

uint32_t avg_r8g8b8(uint32_t a, uint32_t b) {
    return ((a&R8G8B8_MASK_HI)>>1) + ((b&R8G8B8_MASK_HI)>>1) + (a&b&R8G8B8_MASK_LO);
}

再推广一下, 以后我们可能觉得24bit颜色太辣鸡, 推广到30bit. 我们现在使用R10G10B10X2格式, 这个就可以修改为:

#define R10G10B10_MASK_HI ((uint32_t)0x3FEFFBFE)
#define R10G10B10_MASK_LO ((uint32_t)0x00100401)

uint32_t avg_r10g10b10(uint32_t a, uint32_t b) {
    return ((a&R10G10B10_MASK_HI) >> 1) + ((b&R8G8B8_MASK_HI) >> 1) + (a&b&R8G8B8_MASK_LO);
}

不过实际上主要是不透明度的原因, 这个方法在用于包含Alpha信息的颜色时, 会由于Alpha权重问题出现问题. 这个问题引申出一个"预乘Alpha"的概念, 自己被这个东西坑过, 真是记忆犹新。

合并申请

分类: 优化技巧

这个技巧很简单, 将多次内存申请合并到一次, 优点为:

void fx0(void) {
    void* a = malloc(100);
    void* b = malloc(200);
    if (a && b) {
        // XXXXX
    }
    // 虽然free接受空指针, 实际上可能会有类似析构的操作, 还是需要判断
    if (a) free(a);
    if (b) free(b);
}
void fx1(void) {
    void* a = malloc(100);
    if (a) {
        void* b = malloc(200);
        if (b) {
            // XXXXXXXXXXXXXX
            free(b);
        }
        free(a);
    }
}
void gx(void) {
    void* a = malloc(300);
    if (a) {
        void* b = (char*)a + 100;
        // XXXXXXXXXXXXXX
        free(a);
    }
}

缺点, 不适合处理大空间申请, 比如申请两个250MB的内存最好分别申请, 不然地址空间上可能找不到500MB连续空间, 但是能找到两个250MB的连续空间.

还有一个小缺点就是, 现在的运行库配合IDE能够在越界后进行报错. 假如合并, 前者越界会写入下一段空间, 这个非常危险, 并且比较难调试(所以越界检测是非常重要的, 建议使用大量的断言进行处理).

虽然这个技巧很简单, 但是实际上需要直接申请两个独立部分可能比较少见, 常见是"隐式"地多次申请:

struct A {
    B*  b = new(std::nothrow) B;
    C*  c = new(std::nothrow) C;
};

struct D {
    B*  b = new(std::nothrow) B;
};

int main() {
    A a;
    delete(new(std::nothrow) D);
}

这里列举两种情况, A情况就是上述比较明显的, 拥有两次显式的new, 而D的情况则是隐式的两次. 有一点就是我们可以通过replacement new和直接调用析构函数进行内存的再利用, 但是非常注意的是内存对齐的情况, c++目前并没有将c11的aligned_alloc纳入标准(即便纳入按照微软的脾气还是不可能在VS上实现, 主要是允许freealigned_alloc的), 申请对齐的内存需要自己处理又无形地增加了复杂度(好在一般按照8字节对齐就行, 除了特殊要求).

有时会我们会经常的使用"悬浮"状态的对象, 就像D这种, 完全手动控制生命周期的, 其中隐含的多次申请依然可以合并, 只需要找准偏移量即可.

柔性数组, 或者说零长数组(Zero-Length Array) 其实就是这个的一个体现:

struct array_t {
    size_t  length;
    char    data[0];
};

其中类c++扩展是[0], c99标准是[]. c++中, 有时候为了避免兼容问题, 会不写成员变量, 取而代之的是成员函数:

struct array_t {
    size_t  length;
    char* data() { return reinterpret_cast<char*>(this+1) }
};

数组多态

分类: 内部优化

数组一般来说就是储存同一类型的数据, 如果需要多态, 又要保持随机访问可能的就是储存指针数组. 不过内部使用或者强制规定的话, 我们可以规定一个上限, 各个类型的较大值.

enum : size_t { THIS_MAX_SIZE = 256, THIS_MAX_COUNT = 16 };

class Factory {
    template<typename T> T* get_at(size_t i) {
        assert(i < m_count && "out of range");
        return reinterpret_cast<T*>(m_buffer + THIS_MAX_SIZE * i);
    }
    size_t      m_count;
    uint8_t     m_buffer[THIS_MAX_SIZE*THIS_MAX_COUNT];
};

类似这样, 其实就是上一条: 合并申请. 然后手动控制对象构造(replacement new)与析构(调用析构函数), 对于"内部优化"来说, 是允许的.