Skip to content
Published on

시스템 프로그래밍 완전 정복: C언어부터 Rust까지, AI 엔지니어를 위한 저수준 프로그래밍

Authors

개요

AI 엔지니어에게 시스템 프로그래밍은 선택이 아닌 필수입니다. PyTorch C++ extension 작성, CUDA 커널 최적화, 고성능 ML 추론 서버 구축 등 저수준 프로그래밍 능력이 없으면 진정한 성능 최적화는 불가능합니다.

이 가이드는 C언어의 포인터와 메모리 관리부터 Rust의 소유권 시스템, 비동기 프로그래밍, 그리고 AI 엔지니어링 실전 적용까지 체계적으로 다룹니다.


1. 메모리 모델: Stack vs Heap

1.1 스택(Stack) 메모리

스택은 함수 호출 시 자동으로 할당되고 반환 시 자동으로 해제되는 메모리 영역입니다. LIFO(Last In First Out) 구조로 동작하며 크기가 컴파일 타임에 결정되어야 합니다.

#include <stdio.h>

void demonstrate_stack() {
    int x = 10;           // 스택에 4바이트 할당
    double y = 3.14;      // 스택에 8바이트 할당
    char arr[100];        // 스택에 100바이트 할당

    printf("x 주소: %p\n", (void*)&x);
    printf("y 주소: %p\n", (void*)&y);
    printf("arr 주소: %p\n", (void*)arr);
    // 함수 종료 시 위 변수들은 자동으로 해제됨
}

int main() {
    demonstrate_stack();
    // 여기서 x, y, arr는 이미 해제됨
    return 0;
}

1.2 힙(Heap) 메모리

힙은 런타임에 동적으로 할당하는 영역입니다. 크기를 실행 중에 결정할 수 있으나 프로그래머가 직접 해제해야 합니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    float* weights;
    int size;
} Layer;

Layer* create_layer(int size) {
    Layer* layer = (Layer*)malloc(sizeof(Layer));
    if (layer == NULL) {
        fprintf(stderr, "메모리 할당 실패\n");
        return NULL;
    }

    layer->weights = (float*)calloc(size, sizeof(float));
    if (layer->weights == NULL) {
        free(layer);
        return NULL;
    }

    layer->size = size;

    // 가중치 초기화
    for (int i = 0; i < size; i++) {
        layer->weights[i] = (float)rand() / RAND_MAX * 0.1f;
    }

    return layer;
}

void destroy_layer(Layer* layer) {
    if (layer != NULL) {
        free(layer->weights);  // 내부 포인터 먼저 해제
        free(layer);           // 구조체 해제
    }
}

int main() {
    Layer* fc = create_layer(512);
    printf("레이어 크기: %d, 첫 번째 가중치: %.6f\n", fc->size, fc->weights[0]);
    destroy_layer(fc);
    fc = NULL;  // 댕글링 포인터 방지
    return 0;
}

1.3 메모리 레이아웃과 버퍼 오버플로우

#include <stdio.h>
#include <string.h>

// 위험한 함수 - 버퍼 오버플로우 발생 가능
void vulnerable_copy(char* dst, const char* src) {
    strcpy(dst, src);  // 길이 체크 없음!
}

// 안전한 함수
void safe_copy(char* dst, size_t dst_size, const char* src) {
    strncpy(dst, src, dst_size - 1);
    dst[dst_size - 1] = '\0';  // 항상 널 종료 보장
}

int main() {
    char buffer[16];

    // 안전한 복사
    safe_copy(buffer, sizeof(buffer), "Hello, World!");
    printf("복사 결과: %s\n", buffer);

    // 위험한 입력 - 실제로는 절대 하지 말 것
    // vulnerable_copy(buffer, "이 문자열은 16바이트보다 훨씬 깁니다!!!");

    return 0;
}

2. C 언어 핵심: 포인터와 함수 포인터

2.1 포인터 산술

#include <stdio.h>
#include <stdlib.h>

void pointer_arithmetic_demo() {
    int arr[5] = {10, 20, 30, 40, 50};
    int* ptr = arr;

    printf("배열 순회 (포인터 산술):\n");
    for (int i = 0; i < 5; i++) {
        printf("  arr[%d] = %d (주소: %p)\n", i, *(ptr + i), (void*)(ptr + i));
    }

    // 2D 배열을 1D로 접근
    float matrix[3][4];
    float* flat = (float*)matrix;

    for (int i = 0; i < 12; i++) {
        flat[i] = (float)i * 0.5f;
    }

    printf("\n행렬 [1][2] = %.1f\n", matrix[1][2]);  // flat[6]
}

2.2 함수 포인터와 콜백 패턴

ML 프레임워크에서 활성화 함수를 동적으로 선택하는 패턴입니다.

#include <stdio.h>
#include <math.h>

// 활성화 함수 타입 정의
typedef float (*ActivationFn)(float);

float relu(float x) { return x > 0.0f ? x : 0.0f; }
float sigmoid(float x) { return 1.0f / (1.0f + expf(-x)); }
float tanh_act(float x) { return tanhf(x); }

// 레이어에 활성화 함수 적용
void apply_activation(float* data, int n, ActivationFn fn) {
    for (int i = 0; i < n; i++) {
        data[i] = fn(data[i]);
    }
}

// 활성화 함수 팩토리
ActivationFn get_activation(const char* name) {
    if (strcmp(name, "relu") == 0)    return relu;
    if (strcmp(name, "sigmoid") == 0) return sigmoid;
    if (strcmp(name, "tanh") == 0)    return tanh_act;
    return NULL;
}

int main() {
    float data[5] = {-2.0f, -1.0f, 0.0f, 1.0f, 2.0f};

    ActivationFn fn = get_activation("relu");
    apply_activation(data, 5, fn);

    printf("ReLU 결과: ");
    for (int i = 0; i < 5; i++) printf("%.1f ", data[i]);
    printf("\n");

    return 0;
}

3. Rust 소유권 시스템

Rust의 핵심은 컴파일러가 런타임 오버헤드 없이 메모리 안전을 보장한다는 점입니다. 가비지 컬렉터가 없어도 메모리 누수, use-after-free, 데이터 경쟁을 방지합니다.

3.1 소유권(Ownership) 규칙

  1. Rust의 각 값에는 소유자(owner)가 있다
  2. 소유자는 한 번에 하나만 존재한다
  3. 소유자가 스코프를 벗어나면 값이 드롭(drop)된다
fn ownership_basics() {
    // String은 힙에 할당됨
    let s1 = String::from("hello");
    let s2 = s1;  // s1의 소유권이 s2로 이동(move)

    // println!("{}", s1);  // 컴파일 에러! s1은 이미 이동됨
    println!("{}", s2);  // OK

    // 클론으로 깊은 복사
    let s3 = s2.clone();
    println!("s2={}, s3={}", s2, s3);  // 둘 다 유효

    // i32는 Copy trait 구현 - 이동 대신 복사
    let x: i32 = 5;
    let y = x;  // 복사됨
    println!("x={}, y={}", x, y);  // 둘 다 유효
}

3.2 빌림(Borrowing)과 참조

fn calculate_stats(data: &[f32]) -> (f32, f32) {
    let sum: f32 = data.iter().sum();
    let mean = sum / data.len() as f32;

    let variance: f32 = data.iter()
        .map(|x| (x - mean).powi(2))
        .sum::<f32>() / data.len() as f32;

    (mean, variance.sqrt())
}

fn normalize(data: &mut Vec<f32>) {
    let mean: f32 = data.iter().sum::<f32>() / data.len() as f32;
    let std = {
        let var: f32 = data.iter()
            .map(|x| (x - mean).powi(2))
            .sum::<f32>() / data.len() as f32;
        var.sqrt()
    };

    for x in data.iter_mut() {
        *x = (*x - mean) / (std + 1e-8);
    }
}

fn main() {
    let mut weights: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0];

    // 불변 참조 - 소유권 이동 없음
    let (mean, std) = calculate_stats(&weights);
    println!("평균: {:.3}, 표준편차: {:.3}", mean, std);

    // 가변 참조 - 동시에 하나만 허용
    normalize(&mut weights);
    println!("정규화 후: {:?}", weights);
}

3.3 라이프타임(Lifetimes)

라이프타임은 참조의 유효 범위를 명시적으로 표현합니다. 댕글링 포인터를 컴파일 타임에 방지합니다.

// 라이프타임 어노테이션: 반환값이 두 입력 중 더 짧은 생명주기를 가짐
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

struct ModelCache<'a> {
    model_name: &'a str,
    embeddings: Vec<f32>,
}

impl<'a> ModelCache<'a> {
    fn new(name: &'a str, dim: usize) -> Self {
        ModelCache {
            model_name: name,
            embeddings: vec![0.0f32; dim],
        }
    }

    fn get_name(&self) -> &str {
        self.model_name
    }
}

fn lifetime_example() {
    let name = String::from("bert-base");
    let cache = ModelCache::new(&name, 768);
    println!("캐시된 모델: {}", cache.get_name());
    // cache와 name은 같은 스코프에서 유효
}

4. Rust 실전: async/await와 tokio

4.1 비동기 ML 추론 서버

use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use std::sync::Arc;

// 추론 결과 타입
#[derive(Debug)]
struct InferenceResult {
    label: String,
    confidence: f32,
    latency_ms: u64,
}

// 모델 서버 (Arc로 스레드 간 공유)
struct ModelServer {
    model_name: String,
    // 실제로는 candle 또는 ort 모델 포함
}

impl ModelServer {
    fn new(name: &str) -> Self {
        ModelServer { model_name: name.to_string() }
    }

    async fn infer(&self, input: &[f32]) -> InferenceResult {
        // 실제 추론 시뮬레이션
        tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;

        InferenceResult {
            label: "cat".to_string(),
            confidence: 0.95,
            latency_ms: 5,
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let server = Arc::new(ModelServer::new("resnet50"));
    let listener = TcpListener::bind("127.0.0.1:8080").await?;

    println!("ML 추론 서버 시작: 127.0.0.1:8080");

    loop {
        let (mut socket, addr) = listener.accept().await?;
        let server_clone = Arc::clone(&server);

        tokio::spawn(async move {
            let mut buf = vec![0u8; 1024];
            let n = socket.read(&mut buf).await.unwrap_or(0);

            if n > 0 {
                // 더미 입력으로 추론
                let input = vec![0.0f32; 224 * 224 * 3];
                let result = server_clone.infer(&input).await;

                let response = format!(
                    "label={}, confidence={:.3}, latency={}ms\n",
                    result.label, result.confidence, result.latency_ms
                );

                let _ = socket.write_all(response.as_bytes()).await;
                println!("클라이언트 {} 처리 완료", addr);
            }
        });
    }
}

4.2 채널 통신으로 배치 처리

use tokio::sync::mpsc;
use std::time::Instant;

#[derive(Debug)]
struct InferRequest {
    id: u64,
    data: Vec<f32>,
    response_tx: tokio::sync::oneshot::Sender<String>,
}

async fn batch_inference_worker(
    mut rx: mpsc::Receiver<InferRequest>,
    batch_size: usize,
    batch_timeout_ms: u64,
) {
    let mut pending: Vec<InferRequest> = Vec::new();
    let timeout = tokio::time::Duration::from_millis(batch_timeout_ms);

    loop {
        let deadline = tokio::time::Instant::now() + timeout;

        while pending.len() < batch_size {
            match tokio::time::timeout_at(deadline, rx.recv()).await {
                Ok(Some(req)) => pending.push(req),
                Ok(None) => return,  // 채널 닫힘
                Err(_) => break,     // 타임아웃
            }
        }

        if pending.is_empty() { continue; }

        println!("배치 처리: {} 요청", pending.len());

        // 배치 추론 실행 (실제로는 모델 forward pass)
        for req in pending.drain(..) {
            let result = format!("request_{}: label=cat, conf=0.95", req.id);
            let _ = req.response_tx.send(result);
        }
    }
}

4.3 unsafe 코드와 FFI

use std::slice;

// C 라이브러리 함수 선언
extern "C" {
    fn cblas_sgemm(
        order: i32, transa: i32, transb: i32,
        m: i32, n: i32, k: i32,
        alpha: f32,
        a: *const f32, lda: i32,
        b: *const f32, ldb: i32,
        beta: f32,
        c: *mut f32, ldc: i32,
    );
}

// 안전한 래퍼 함수
pub fn matrix_multiply(
    a: &[f32], b: &[f32], c: &mut [f32],
    m: usize, n: usize, k: usize,
) {
    assert_eq!(a.len(), m * k);
    assert_eq!(b.len(), k * n);
    assert_eq!(c.len(), m * n);

    unsafe {
        cblas_sgemm(
            101,  // CblasRowMajor
            111,  // CblasNoTrans
            111,  // CblasNoTrans
            m as i32, n as i32, k as i32,
            1.0,
            a.as_ptr(), k as i32,
            b.as_ptr(), n as i32,
            0.0,
            c.as_mut_ptr(), n as i32,
        );
    }
}

// 원시 포인터로 슬라이스 생성 (FFI 경계에서 활용)
pub unsafe fn tensor_from_raw(ptr: *const f32, len: usize) -> &'static [f32] {
    slice::from_raw_parts(ptr, len)
}

5. 시스템 프로그래밍: 파일 I/O와 프로세스

5.1 파일 시스템 I/O (C)

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

// 이진 파일로 모델 가중치 저장/로드
typedef struct {
    uint32_t magic;     // 파일 형식 식별자
    uint32_t version;
    uint32_t num_layers;
    uint32_t total_params;
} ModelHeader;

int save_weights(const char* path, float* weights, int count) {
    FILE* f = fopen(path, "wb");
    if (!f) return -1;

    ModelHeader header = {
        .magic = 0x4D4C4D44,  // "MLMD"
        .version = 1,
        .num_layers = 1,
        .total_params = (uint32_t)count
    };

    fwrite(&header, sizeof(header), 1, f);
    fwrite(weights, sizeof(float), count, f);
    fclose(f);
    return 0;
}

float* load_weights(const char* path, int* count) {
    FILE* f = fopen(path, "rb");
    if (!f) return NULL;

    ModelHeader header;
    if (fread(&header, sizeof(header), 1, f) != 1) {
        fclose(f);
        return NULL;
    }

    if (header.magic != 0x4D4C4D44) {
        fprintf(stderr, "잘못된 파일 형식\n");
        fclose(f);
        return NULL;
    }

    *count = (int)header.total_params;
    float* weights = (float*)malloc(*count * sizeof(float));
    fread(weights, sizeof(float), *count, f);
    fclose(f);
    return weights;
}

5.2 스레드와 뮤텍스 (C - pthreads)

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    float* data;
    int start;
    int end;
    float result;
} SumArgs;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
float global_sum = 0.0f;

void* parallel_sum(void* arg) {
    SumArgs* args = (SumArgs*)arg;
    float local_sum = 0.0f;

    for (int i = args->start; i < args->end; i++) {
        local_sum += args->data[i];
    }

    pthread_mutex_lock(&mutex);
    global_sum += local_sum;
    pthread_mutex_unlock(&mutex);

    args->result = local_sum;
    return NULL;
}

float parallel_reduce(float* data, int n, int num_threads) {
    pthread_t* threads = malloc(num_threads * sizeof(pthread_t));
    SumArgs* args = malloc(num_threads * sizeof(SumArgs));
    int chunk = n / num_threads;

    global_sum = 0.0f;

    for (int t = 0; t < num_threads; t++) {
        args[t].data = data;
        args[t].start = t * chunk;
        args[t].end = (t == num_threads - 1) ? n : (t + 1) * chunk;
        pthread_create(&threads[t], NULL, parallel_sum, &args[t]);
    }

    for (int t = 0; t < num_threads; t++) {
        pthread_join(threads[t], NULL);
    }

    free(threads);
    free(args);
    return global_sum;
}

6. AI 엔지니어링 연계

6.1 PyTorch C++ Extension

// fast_ops.cpp - PyTorch C++ 확장 모듈
#include <torch/extension.h>
#include <vector>

// ReLU 커스텀 구현
torch::Tensor fast_relu(torch::Tensor input) {
    TORCH_CHECK(input.dtype() == torch::kFloat32,
                "fast_relu: float32 텐서만 지원합니다, 받은 타입: ",
                input.dtype());
    TORCH_CHECK(input.is_contiguous(),
                "fast_relu: contiguous 텐서가 필요합니다");

    auto output = torch::empty_like(input);
    auto* in_ptr = input.data_ptr<float>();
    auto* out_ptr = output.data_ptr<float>();
    int64_t n = input.numel();

    for (int64_t i = 0; i < n; i++) {
        out_ptr[i] = in_ptr[i] > 0.0f ? in_ptr[i] : 0.0f;
    }

    return output;
}

// 행렬 곱 + 편향 추가 (fused operation)
torch::Tensor linear_forward(
    torch::Tensor input,
    torch::Tensor weight,
    torch::Tensor bias
) {
    TORCH_CHECK(input.dim() == 2, "입력은 2D 텐서여야 합니다");
    TORCH_CHECK(weight.dim() == 2, "가중치는 2D 텐서여야 합니다");
    TORCH_CHECK(input.size(1) == weight.size(1),
                "입력과 가중치의 차원이 맞지 않습니다");

    // torch::mm + bias 추가
    auto output = torch::mm(input, weight.t());
    if (bias.defined()) {
        output += bias.unsqueeze(0);
    }
    return output;
}

// Python에 바인딩할 함수 등록
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
    m.def("fast_relu", &fast_relu, "빠른 ReLU 구현");
    m.def("linear_forward", &linear_forward, "선형 레이어 순전파");
}

6.2 CUDA 커널 작성

// cuda_kernels.cu - CUDA C 커널
#include <cuda_runtime.h>
#include <stdio.h>

// GPU에서 병렬로 ReLU 실행
__global__ void relu_kernel(
    const float* __restrict__ input,
    float* __restrict__ output,
    int n
) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;

    if (idx < n) {
        output[idx] = input[idx] > 0.0f ? input[idx] : 0.0f;
    }
}

// Softmax (수치 안정성 포함)
__global__ void softmax_kernel(
    const float* __restrict__ input,
    float* __restrict__ output,
    int batch_size,
    int num_classes
) {
    int batch_idx = blockIdx.x;
    if (batch_idx >= batch_size) return;

    const float* in = input + batch_idx * num_classes;
    float* out = output + batch_idx * num_classes;

    // 최대값 찾기 (수치 안정성)
    float max_val = in[0];
    for (int i = 1; i < num_classes; i++) {
        max_val = fmaxf(max_val, in[i]);
    }

    // exp 합산
    float sum = 0.0f;
    for (int i = 0; i < num_classes; i++) {
        out[i] = expf(in[i] - max_val);
        sum += out[i];
    }

    // 정규화
    for (int i = 0; i < num_classes; i++) {
        out[i] /= sum;
    }
}

// 호스트 함수 (C에서 호출)
void launch_relu(const float* d_in, float* d_out, int n) {
    int threads = 256;
    int blocks = (n + threads - 1) / threads;
    relu_kernel<<<blocks, threads>>>(d_in, d_out, n);
    cudaDeviceSynchronize();
}

6.3 Rust candle로 ML 추론 서버

// candle_inference.rs - Rust로 LLM 추론
use candle_core::{Device, Tensor, DType};
use candle_nn::{Linear, Module, VarBuilder};
use std::path::Path;

struct SimpleTransformerBlock {
    attention: Linear,
    feed_forward: Linear,
    norm: Vec<f32>,
}

impl SimpleTransformerBlock {
    fn new(vb: VarBuilder, hidden_dim: usize) -> candle_core::Result<Self> {
        let attention = candle_nn::linear(hidden_dim, hidden_dim, vb.pp("attention"))?;
        let feed_forward = candle_nn::linear(hidden_dim, hidden_dim * 4, vb.pp("ffn"))?;
        let norm = vec![1.0f32; hidden_dim];
        Ok(Self { attention, feed_forward, norm })
    }

    fn forward(&self, x: &Tensor) -> candle_core::Result<Tensor> {
        // Self-attention (단순화)
        let attn_out = self.attention.forward(x)?;
        let x = (x + attn_out)?;

        // FFN
        let ffn_out = self.feed_forward.forward(&x)?;
        let ffn_out = ffn_out.relu()?;

        Ok(ffn_out)
    }
}

async fn run_inference(model_path: &Path, input_ids: &[u32])
    -> candle_core::Result<Vec<f32>>
{
    let device = Device::cuda_if_available(0)?;
    println!("디바이스: {:?}", device);

    // 입력 텐서 생성
    let input = Tensor::new(input_ids, &device)?;
    let input = input.unsqueeze(0)?;  // 배치 차원 추가

    // 임베딩 테이블 (예시)
    let vocab_size = 32000usize;
    let hidden_dim = 768usize;
    let embedding = Tensor::randn(0f32, 1.0, (vocab_size, hidden_dim), &device)?;

    // 임베딩 룩업
    let hidden = embedding.index_select(&input.flatten_all()?, 0)?;

    // 마지막 레이어 로짓 추출
    let logits = hidden.mean(1)?;  // 시퀀스 평균
    let logits_vec: Vec<f32> = logits.flatten_all()?.to_vec1()?;

    Ok(logits_vec)
}

7. 성능 최적화: SIMD와 Cache-Friendly 코드

7.1 SIMD 벡터화

#include <immintrin.h>  // AVX2
#include <stdio.h>

// 일반 구현
float dot_product_scalar(const float* a, const float* b, int n) {
    float sum = 0.0f;
    for (int i = 0; i < n; i++) {
        sum += a[i] * b[i];
    }
    return sum;
}

// AVX2 SIMD 구현 (8개 float 동시 처리)
float dot_product_avx2(const float* a, const float* b, int n) {
    __m256 sum_vec = _mm256_setzero_ps();
    int i = 0;

    // 8개씩 처리
    for (; i <= n - 8; i += 8) {
        __m256 va = _mm256_loadu_ps(a + i);
        __m256 vb = _mm256_loadu_ps(b + i);
        sum_vec = _mm256_fmadd_ps(va, vb, sum_vec);  // FMA: a*b+c
    }

    // 8개 합산
    __m128 lo = _mm256_extractf128_ps(sum_vec, 0);
    __m128 hi = _mm256_extractf128_ps(sum_vec, 1);
    __m128 sum128 = _mm_add_ps(lo, hi);
    sum128 = _mm_hadd_ps(sum128, sum128);
    sum128 = _mm_hadd_ps(sum128, sum128);

    float result = _mm_cvtss_f32(sum128);

    // 나머지 처리
    for (; i < n; i++) {
        result += a[i] * b[i];
    }

    return result;
}

7.2 Cache-Friendly 행렬 전치

// 캐시 비친화적: 열 접근
void matrix_multiply_naive(float* C, const float* A, const float* B,
                            int M, int N, int K) {
    for (int i = 0; i < M; i++)
        for (int j = 0; j < N; j++)
            for (int k = 0; k < K; k++)
                C[i*N+j] += A[i*K+k] * B[k*N+j];  // B 접근이 캐시 미스 많음
}

// 캐시 친화적: 블록 행렬 곱셈
void matrix_multiply_blocked(float* C, const float* A, const float* B,
                              int M, int N, int K, int block_size) {
    for (int ii = 0; ii < M; ii += block_size)
        for (int jj = 0; jj < N; jj += block_size)
            for (int kk = 0; kk < K; kk += block_size)
                for (int i = ii; i < ii+block_size && i < M; i++)
                    for (int j = jj; j < jj+block_size && j < N; j++)
                        for (int k = kk; k < kk+block_size && k < K; k++)
                            C[i*N+j] += A[i*K+k] * B[k*N+j];
}

7.3 Rust에서의 성능 최적화

use std::time::Instant;

// #[inline(always)]로 인라이닝 강제
#[inline(always)]
fn relu_fast(x: f32) -> f32 {
    x.max(0.0)
}

// 반복자 체인은 LLVM이 자동 벡터화
fn batch_relu(data: &mut [f32]) {
    data.iter_mut().for_each(|x| *x = relu_fast(*x));
}

// unsafe로 경계 검사 생략 (검증된 경우에만)
fn dot_product_unchecked(a: &[f32], b: &[f32]) -> f32 {
    assert_eq!(a.len(), b.len());
    let n = a.len();
    let mut sum = 0.0f32;

    unsafe {
        for i in 0..n {
            sum += a.get_unchecked(i) * b.get_unchecked(i);
        }
    }
    sum
}

fn benchmark_relu() {
    let mut data: Vec<f32> = (0..1_000_000)
        .map(|i| (i as f32 - 500_000.0) / 1000.0)
        .collect();

    let start = Instant::now();
    batch_relu(&mut data);
    println!("ReLU 1M 요소: {:?}", start.elapsed());
}

8. 퀴즈

Q1. Rust에서 ownership이 이전(move)되는 상황과 Copy trait의 차이점은?

정답: Copy trait을 구현한 타입은 이동(move) 대신 복사(copy)가 발생하며 원본도 사용 가능합니다.

설명: String, Vec, Box 등 힙 메모리를 사용하는 타입은 ownership이 이전됩니다. 반면 i32, f32, bool, char 등 스택에만 존재하는 고정 크기 타입은 Copy trait을 구현하여 자동으로 복사됩니다. Clone은 명시적 깊은 복사, Copy는 암묵적 비트 복사입니다.

Q2. C에서 use-after-free 버그가 발생하는 조건과 방지 방법은?

정답: free() 호출 후 해제된 메모리 포인터를 사용하면 발생합니다.

설명: free(ptr)ptr이 여전히 이전 주소를 가리키면(댕글링 포인터) 해당 주소를 읽거나 쓸 때 undefined behavior가 발생합니다. 방지법: 1) free(ptr) 직후 ptr = NULL 설정, 2) if(ptr != NULL) 검사 후 사용, 3) AddressSanitizer(ASan)로 디버깅, 4) Rust처럼 소유권 추적 도구 사용.

Q3. Rust의 lifetime 어노테이션이 반드시 필요한 상황은?

정답: 함수가 참조를 반환할 때 반환값의 수명이 어떤 입력 참조와 연관되는지 컴파일러가 추론할 수 없는 경우입니다.

설명: 예를 들어 fn longest(x: &str, y: &str) -> &str처럼 두 참조 중 하나를 반환할 때, 컴파일러는 반환값의 유효 기간을 알 수 없습니다. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str로 명시하면 반환값의 수명이 두 입력 중 짧은 것과 동일함을 보장합니다.

Q4. SIMD 명령어가 성능을 향상시키는 원리와 한계는?

정답: 하나의 CPU 명령어로 여러 데이터를 동시에 처리(데이터 수준 병렬성)하여 처리량을 높입니다.

설명: AVX2는 256비트 레지스터로 8개 float32를 동시 처리합니다. FMA(Fused Multiply-Add)는 곱셈과 덧셈을 단일 명령으로 실행합니다. 한계: 조건 분기, 메모리 비정렬, 데이터 의존성이 있으면 효과가 줄어듭니다. Rust에서는 std::simd crate이나 LLVM 자동 벡터화를 활용할 수 있습니다.

Q5. PyTorch C++ extension에서 TORCH_CHECK 매크로의 역할은?

정답: 조건이 false일 때 명확한 에러 메시지와 함께 예외를 발생시켜 잘못된 입력을 조기에 탐지합니다.

설명: TORCH_CHECK(condition, message)는 조건이 false이면 c10::Error를 throw합니다. 일반 C++ assert와 달리 릴리즈 빌드에서도 작동하며, Python 예외로 변환되어 사용자에게 명확한 에러를 전달합니다. dtype 검사, shape 검사, contiguous 검사 등에 활용합니다.


9. 학습 로드맵

시스템 프로그래밍을 AI 엔지니어 관점에서 학습하는 순서를 제안합니다.

1단계 - C 기초 (2-4주): K&R "The C Programming Language", 포인터와 메모리 관리, Makefile과 CMake

2단계 - Rust 입문 (4-6주): "The Rust Programming Language" (공식 책), ownership/borrowing/lifetimes, cargo 생태계

3단계 - 시스템 개념 (2-4주): 운영체제 기초 (프로세스, 스레드, 신호), 파일 I/O, 소켓 프로그래밍

4단계 - AI 연계 (4-8주): PyTorch C++ extension 작성, CUDA 기초 커널 작성, candle 또는 ort로 Rust 추론 서버

5단계 - 성능 최적화 (지속): perf/flamegraph 프로파일링, SIMD 최적화, cache-aware 알고리즘


마무리

시스템 프로그래밍은 AI 인프라의 근간입니다. C언어는 하드웨어와 직접 소통하는 언어로 CUDA, 드라이버, 임베디드 시스템에 여전히 필수입니다. Rust는 C의 성능을 유지하면서 메모리 안전을 컴파일 타임에 보장하여 새로운 시스템 소프트웨어의 표준이 되어가고 있습니다.

AI 모델이 아무리 정교해져도 그것을 실제로 돌리는 인프라는 시스템 프로그래밍으로 만들어집니다. PyTorch 자체가 C++와 CUDA로 이루어진 거대한 시스템 소프트웨어입니다. 저수준을 이해하는 AI 엔지니어가 진정한 성능 차이를 만들어낼 수 있습니다.