phop 의 foreach 문 제 를 깊이 분석 하 다.

17806 단어 phpforeach
선언:php 4 에 foreach 구 조 를 도 입 했 는데 이것 은 그룹 을 옮 겨 다 니 는 간단 한 방식 이다.전통 적 인 for 순환 에 비해 foreach 는 키 쌍 을 더욱 편리 하 게 얻 을 수 있 습 니 다.php 5 이전에 foreach 는 배열 에 만 사용 할 수 있 습 니 다.php 5 이후 foreach 를 이용 하여 대상 을 옮 겨 다 닐 수 있 습 니 다.본문 에 서 는 배열 을 옮 겨 다 니 는 상황 만 토론 한다.foreach 는 간단 하지만 의외 의 행동 이 발생 할 수 있 습 니 다.특히 코드 가 인용 과 관련 된 상황 에서.다음은 몇 가지 케이스 를 열거 하여 우리 가 foreach 의 본질 을 더욱 잘 인식 하 는 데 도움 이 된다.문제 1:

$arr = array(1,2,3);
foreach($arr as $k => &$v) {
    $v = $v * 2;
}
// now $arr is array(2, 4, 6)
foreach($arr as $k => $v) {
    echo "$k", " => ", "$v";
}
먼저 간단 한 것 부터 시작 합 니 다.만약 에 우리 가 상기 코드 를 실행 하려 고 시도 하면 마지막 출력 이 0=>2 인 것 을 발견 할 수 있 습 니 다.  1=>4  2=>4 。왜 0=>2 가 아 닙 니까?  1=>4  2=>6 ?사실,우 리 는 foreach($arr as$k=>$v)구조 가 다음 과 같은 조작 을 포함 하고 있다 고 볼 수 있 습 니 다.각각 배열 의 현재'키'와 현재'값'을 변수$k 와$v 에 부여 합 니 다.구체 적 인 전개 형 예 를 들 어

foreach($arr as $k => $v){
    // 2
    $v = currentVal();
    $k = currentKey();
    //
    ……
}
상기 이론 에 따 르 면 지금 우 리 는 다음 의 foreach:첫 번 째 순환,$v 는 하나의 인용 이기 때문에$v=&$arr[0],$v=$v*2 는$arr[0]*2 에 해당 하기 때문에$arr 은 2,2,3 번 째 순환,$v=&$arr[1],$arr 은 2,4,3 번 째 순환,$v=&$arr[2],$arr 은 2,4 로 변 한다.6.그 후에 코드 는 두 번 째 foreach 에 들 어 갔다.첫 번 째 순환 은$v=$arr[0]을 포함 하여 촉발 되 었 다.이때$v 는$arr[2]의 인용,즉$arr[2]=$arr[0],$arr 은 2,4,2 번 째 순환 이 되 었 고$v=$arr[1],즉$arr[2]=$arr[1],$arr 는 2,4,4 번 째 순환 이 되 었 으 며$v=$arr[2],즉$arr[2],$arr 는 2,4,4 OK 가 되 어 분석 이 완료 되 었 다.어떻게 비슷 한 문 제 를 해결 합 니까?php 매 뉴 얼 에 있 는 알림:Warning:배열 의 마지막 요소 인$value 는 foreach 순환 후에 도 유 지 됩 니 다.unset()를 사용 하여 소각 하 는 것 을 권장 합 니 다.

$arr = array(1,2,3);
foreach($arr as $k => &$v) {
    $v = $v * 2;
}
unset($v);
foreach($arr as $k => $v) {
    echo "$k", " => ", "$v";
}
// 0=>2  1=>4  2=>6
은 이 문제 에서 인용 이 부작용 을 동반 할 가능성 이 높다 는 것 을 알 수 있다.무의식 적 인 수정 으로 인해 배열 의 내용 이 변경 되 지 않 으 려 면 이 인용 을 제때에 취소 하 는 것 이 좋다.문제 2:

$arr = array('a','b','c');
foreach($arr as $k => $v) {
    echo key($arr), "=>", current($arr);
}
// 1=>b 1=>b 1=>b
이 문 제 는 더욱 기괴 하 다.매 뉴 얼 에 따 르 면 키 와 current 는 각각 배열 의 현재 요 소 를 가 져 오 는 키 입 니 다.그런데 왜 키($arr)는 항상 1,current($arr)는 항상 b 일 까요?컴 파일 된 opcode: 을 vld 로 먼저 봅 니 다.세 번 째 줄 의 ASSIGN 명령 을 보면 array('a','b','c')를$arr 에 할당 하 는 것 을 의미 합 니 다.$arr 는 CV 이 고 array('a','b','c')는 TMP 이기 때문에 ASSIGN 명령 에서 실제 실 행 된 함 수 를 찾 으 면 ZEND 입 니 다.ASSIGN_SPEC_CV_TMP_HANDLER。여기 서 특별히 지적 해 야 할 것 은 CV 는 PHP 5.1 이후 에 야 추 가 된 변수 cache 입 니 다.이것 은 zval**를 배열 형식 으로 저장 합 니 다.cache 에 사 는 변 수 를 다시 사용 할 때 active 기호 표를 찾 지 않 고 CV 배열 에서 직접 가 져 옵 니 다.배열 의 접근 속도 가 hash 표를 훨씬 초과 하기 때문에 효율 을 높 일 수 있 습 니 다.

static int ZEND_FASTCALL  ZEND_ASSIGN_SPEC_CV_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    zend_op *opline = EX(opline);
    zend_free_op free_op2;
    zval *value = _get_zval_ptr_tmp(&opline->op2, EX(Ts), &free_op2 TSRMLS_CC);

    // CV $arr**
    zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_W TSRMLS_CC);
    if (IS_CV == IS_VAR && !variable_ptr_ptr) {
        ……
    }
    else {
        // array $arr
         value = zend_assign_to_variable(variable_ptr_ptr, value, 1 TSRMLS_CC);
        if (!RETURN_VALUE_UNUSED(&opline->result)) {
            AI_SET_PTR(EX_T(opline->result.u.var).var, value);
            PZVAL_LOCK(value);
        }
    }
    ZEND_VM_NEXT_OPCODE();
}
ASSIGN 명령 이 완 료 된 후에 CV 배열 에 zval**지침 이 추가 되 었 고 포인터 가 실제 array 를 가리 키 는 것 은$arr 가 CV 캐 시 되 었 음 을 나타 낸다. 다음 배열 의 순환 작업 을 수행 합 니 다.FE 를 보 겠 습 니 다.RESET 명령 에 대응 하 는 실행 함 수 는 ZEND 입 니 다.FE_RESET_SPEC_CV_HANDLER:

static int ZEND_FASTCALL  ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    ……
    if (……) {
        ……
    } else {
        // CV array
        array_ptr = _get_zval_ptr_cv(&opline->op1, EX(Ts), BP_VAR_R TSRMLS_CC);
        ……
    }
    ……
    // array zend_execute_data->Ts (Ts temp_variable)
    AI_SET_PTR(EX_T(opline->result.u.var).var, array_ptr);
    PZVAL_LOCK(array_ptr);
    if (iter) {
        ……
    } else if ((fe_ht = HASH_OF(array_ptr)) != NULL) {
        //
        zend_hash_internal_pointer_reset(fe_ht);
        if (ce) {
            ……
        }
        is_empty = zend_hash_has_more_elements(fe_ht) != SUCCESS;

        // EX_T(opline->result.u.var).fe.fe_pos
        zend_hash_get_pointer(fe_ht, &EX_T(opline->result.u.var).fe.fe_pos);
    } else {
        ……
    }
    ……
}
여 기 는 주로 중요 한 지침 2 개 를 zend 에 저장 합 니 다.execute_data->Ts 중:•EXT(opline->result.u.var).var----array 를 가리 키 는 지침•EXT(opline->result.u.var).fe.fe_pos---array 내부 요 소 를 가리 키 는 포인터 FERESET 명령 이 실 행 된 후 메모리 의 실제 상황 은 다음 과 같 습 니 다.
이어서 FE 를 계속 살 펴 보 겠 습 니 다.FETCH,이에 대응 하 는 실행 함 수 는 ZEND 입 니 다.FE_FETCH_SPEC_VAR_HANDLER:

static int ZEND_FASTCALL  ZEND_FE_FETCH_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    zend_op *opline = EX(opline);

    // EX_T(opline->op1.u.var).var.ptr
    zval *array = EX_T(opline->op1.u.var).var.ptr;
    ……

    switch (zend_iterator_unwrap(array, &iter TSRMLS_CC)) {
        default:
        case ZEND_ITER_INVALID:
            ……
        case ZEND_ITER_PLAIN_OBJECT: {
            ……
        }
        case ZEND_ITER_PLAIN_ARRAY:
            fe_ht = HASH_OF(array);

            // :
            // FE_RESET EX_T(opline->op1.u.var).fe.fe_pos
            //
            zend_hash_set_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos);

            //
            if (zend_hash_get_current_data(fe_ht, (void **) &value)==FAILURE) {
                ZEND_VM_JMP(EX(op_array)->opcodes+opline->op2.u.opline_num);
            }
            if (use_key) {
                key_type = zend_hash_get_current_key_ex(fe_ht, &str_key, &str_key_len, &int_key, 1, NULL);
            }

            //
            zend_hash_move_forward(fe_ht);

            // EX_T(opline->op1.u.var).fe.fe_pos
            zend_hash_get_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos);
            break;
        case ZEND_ITER_OBJECT:
            ……
    }

    ……
}
근거 FEFETCH 의 실현 은 foreach($arr as$k=>$v)가 하 는 일 을 대체적으로 알 게 되 었 습 니 다.zendexecute_data->Ts 의 지침 은 배열 요 소 를 가 져 오고 성공 한 후에 이 지침 을 다음 위치 로 이동 하여 다시 저장 합 니 다.

쉽게 말 하면 첫 번 째 순환 중 FEFETCH 에서 배열 의 내부 지침 을 두 번 째 요소 로 이동 시 켰 기 때문에 foreach 내부 에서 key($arr)와 current($arr)를 호출 할 때 실제 적 으로 얻 은 것 은 1 과'b'입 니 다.그런데 왜 1=>b 를 세 번 출력 합 니까?9 번 줄 과 13 번 줄 의 SEND 를 계속 보 겠 습 니 다.REF 명령 은$arr 인 자 를 스 택 에 저장 하 는 것 을 표시 합 니 다.이어서 보통 DO 를 사용 합 니 다.FCALL 명령 은 key 와 current 함 수 를 호출 합 니 다.PHP 는 로 컬 기기 코드 로 컴 파일 되 지 않 았 기 때문에 php 는 이러한 opcode 명령 을 사용 하여 실제 CPU 와 메모리 의 작업 방식 을 모 의 합 니 다.PHP 원본 코드 의 SEND 찾기REF:

static int ZEND_FASTCALL  ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    ……
    // CV $arr
    varptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_W TSRMLS_CC);
    ……

    // , copy array key
    SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr);
    varptr = *varptr_ptr;
    Z_ADDREF_P(varptr);

    //
    zend_vm_stack_push(varptr TSRMLS_CC);
    ZEND_VM_NEXT_OPCODE();
}
상기 코드 중의 SEPARATEZVAL_TO_MAKE_IS_REF 는 매크로:

#define SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv)    \
    if (!PZVAL_IS_REF(*ppzv)) {                \
        SEPARATE_ZVAL(ppzv);                \
        Z_SET_ISREF_PP((ppzv));                \
    }
SEPARATEZVAL_TO_MAKE_IS_REF 의 주요 역할 은 변수 가 인용 이 아니라면 메모리 에 새 것 을 복사 하 는 것 이다.이 예 에서 array('a','b','c')를 복사 하 였 습 니 다.따라서 변 수 를 분리 한 후의 메모 리 는 입 니 다.변 수 를 분리 한 후에 CV 배열 의 지침 은 새로운 copy 를 통 해 나 온 데 이 터 를 가리 키 고 zendexecute_data->Ts 의 지침 은 오래된 데 이 터 를 얻 을 수 있 습 니 다.다음 순환 은 일일이 설명 하지 않 겠 습 니 다.위의 그림 과 결합 하면 foreach 구 조 는 아래 파란색 array 를 사용 합 니 다.a,b,c·key,current 는 위 노란색 array 를 사용 합 니 다.내부 지침 은 b 를 영원히 가리 키 고 있 습 니 다.우 리 는 왜 key 와 current 가 array 의 두 번 째 요 소 를 되 돌려 주 는 지 알 게 되 었 습 니 다.외부 코드 가 copy 에 작용 하 는 array 가 없 기 때 문 입 니 다.그것 의 내부 지침 은 영원히 움 직 이지 않 을 것 이다.문제 3:

$arr = array('a','b','c');
foreach($arr as $k => &$v) {
    echo key($arr), '=>', current($arr);
}// 1=>b 2=>c =>
본 문제 와 문제 2 는 약간의 차이 만 있다.본 문제 의 foreach 는 인용 을 사용 했다.VLD 로 본 문 제 를 살 펴 보 니 문제 2 코드 로 컴 파일 된 opcode 와 같 습 니 다.따라서 우 리 는 문제 2 의 추적 방법 을 이용 하여 opcode 에 대응 하 는 실현 을 점차적으로 살 펴 본다.우선 foreach 는 FE 를 호출 합 니 다.RESET:

static int ZEND_FASTCALL  ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    ……
    if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {
        // CV
        array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_R TSRMLS_CC);
        if (array_ptr_ptr == NULL || array_ptr_ptr == &EG(uninitialized_zval_ptr)) {
            ……
        }
        else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) {
            ……
        }
        else {
            // array
            if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) {
                SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr);
                if (opline->extended_value & ZEND_FE_FETCH_BYREF) {
                    // array zval is_ref
                    Z_SET_ISREF_PP(array_ptr_ptr);
                }
            }
            array_ptr = *array_ptr_ptr;
            Z_ADDREF_P(array_ptr);
        }
    } else {
        ……
    }
    ……
}
문제 2 에서 FE 일 부 를 분 석 했 습 니 다.RESET 의 실현.특별한 주의 가 필요 합 니 다.이 foreach 획득 값 은 인용 을 사 용 했 기 때문에 실행 할 때 FERESET 에 서 는 이전 문제 와 다른 다른 지점 으로 들 어 갑 니 다.결국,FERESET 는 array 의 is 를ref 는 true 로 설정 되 어 있 습 니 다.이 때 메모리 에 array 의 데이터 만 있 습 니 다.다음은 SEND 분석REF:

static int ZEND_FASTCALL  ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    ……
    // CV $arr
    varptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_W TSRMLS_CC);
    ……

    // , CV , copy array
    SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr);
    varptr = *varptr_ptr;
    Z_ADDREF_P(varptr);

    //
    zend_vm_stack_push(varptr TSRMLS_CC);
    ZEND_VM_NEXT_OPCODE();
}
매크로 SEPARATEZVAL_TO_MAKE_IS_REF 분리 isref=false 의 변수.이전에 array 가 is 설정 되 었 기 때문에ref=true,따라서 복사 본 은 복사 되 지 않 습 니 다.메모리 에 array 데이터 가 하나 밖 에 없다 는 얘 기다.

위의 그림 은 앞의 2 차 순환 이 왜 1=>b 2=>C 를 출력 하 는 지 설명 한다.3 차 순환 FEFETCH 때 는 포인 터 를 계속 앞으로 이동 합 니 다.

ZEND_API int zend_hash_move_forward_ex(HashTable *ht, HashPosition *pos)
{
    HashPosition *current = pos ? pos : &ht->pInternalPointer;
    IS_CONSISTENT(ht);
    if (*current) {
        *current = (*current)->pListNext;
        return SUCCESS;
    } else
        return FAILURE;
}
이때 내부 포인터 가 배열 의 마지막 요 소 를 가리 키 기 때문에 앞으로 이동 하면 NULL 을 가리 킬 것 입 니 다.내부 포인 터 를 NULL 에 가리 키 면 배열 에 key 와 current 를 호출 하면 각각 NULL 과 false 로 돌아 가 호출 에 실 패 했 음 을 표시 합 니 다.이 때 echo 에서 문 자 를 내지 않 습 니 다. 문제 4:

$arr = array(1, 2, 3);
$tmp = $arr;
foreach($tmp as $k => &$v){
    $v *= 2;
}
var_dump($arr, $tmp); // ?
이 문 제 는 foreach 와 관계 가 크 지 않 지만 foreach 와 관련 된 이상 함께 토론 합 시다.)코드 에 먼저 배열$arr 를 만 든 다음 에 이 배열 을$tmp 에 부 여 했 습 니 다.다음 foreach 순환 에서$v 를 수정 하면 배열$tmp 에 작용 하지만$arr 에는 작용 하지 않 습 니 다.왜 일 까요?이것 은 php 에서 할당 연산 은 한 변수의 값 을 다른 변수 에 복사 하기 때문에 그 중 하 나 를 수정 하면 다른 변수 에 영향 을 주지 않 습 니 다.주제:이것 은 object 형식 에 적용 되 지 않 습 니 다.PHP 5 부터 대상 의 기본 값 은 인용 을 통 해 할당 합 니 다.예 를 들 어

class A{
    public $foo = 1;
}
$a1 = $a2 = new A;
$a1->foo=100;
echo $a2->foo; // 100,$a1 $a2
에서 문제 의 코드 로 돌아 갑 니 다.지금 우 리 는$tmp=$arr 가 사실은 값 복사 라 는 것 을 확인 할 수 있 습 니 다.전체$arr 배열 은$tmp 에 다시 복 사 됩 니 다.이론 적 으로 할당 문 구 를 실행 한 후에 메모리 에 똑 같은 배열 이 2 개 있 을 것 이다.만약 수조 가 매우 크다 면 이런 조작 이 매우 느 리 지 않 겠 느 냐 는 동창 회 의 의문 이 있 을 지도 모른다.다행히 phop 은 더 똑똑 한 처리 방법 이 있 습 니 다.실제로$tmp=$arr 가 실 행 된 후에 도 메모리 에는 array 가 하나 밖 에 없습니다.php 소스 코드 의 zend 보기assign_to_variable 구현(phop 5.3.26 에서 따 온 것):

static inline zval* zend_assign_to_variable(zval **variable_ptr_ptr, zval *value, int is_tmp_var TSRMLS_DC)
{
    zval *variable_ptr = *variable_ptr_ptr;
    zval garbage;
    ……
  // object
    if (Z_TYPE_P(variable_ptr) == IS_OBJECT && Z_OBJ_HANDLER_P(variable_ptr, set)) {
        ……
    }
    //
    if (PZVAL_IS_REF(variable_ptr)) {
        ……
    } else {
        // refcount__gc=1
        if (Z_DELREF_P(variable_ptr)==0) {
            ……
        } else {
            GC_ZVAL_CHECK_POSSIBLE_ROOT(*variable_ptr_ptr);
            //
            if (!is_tmp_var) {
                if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > 0) {
                    ALLOC_ZVAL(variable_ptr);
                    *variable_ptr_ptr = variable_ptr;
                    *variable_ptr = *value;
                    Z_SET_REFCOUNT_P(variable_ptr, 1);
                    zval_copy_ctor(variable_ptr);
                } else {
                    // $tmp=$arr ,
                    // value $arr array ,variable_ptr_ptr $tmp
                    // ,
                    *variable_ptr_ptr = value;
                    // value refcount__gc +1, refcount__gc 1,Z_ADDREF_P 2
                    Z_ADDREF_P(value);
                }
            } else {
                ……
            }
        }
        Z_UNSET_ISREF_PP(variable_ptr_ptr);
    }
    return *variable_ptr_ptr;
}
에서$tmp=$arr 의 본질은 array 의 지침 을 복사 한 다음 array 의 refcount 를 자동 으로 1.그림 으로 표현 하 는 것 입 니 다.여전히 하나의 array 배열 만 있 습 니 다. array 만 있 는 이상 foreach 순환 에서$tmp 를 수정 할 때 왜$arr 는 변 하지 않 았 습 니까?PHP 소스 의 ZEND 계속 보기FE_RESET_SPEC_CV_HANDLER 함수,이것 은 OPCODE HANDLER 입 니 다.이에 대응 하 는 OPCODE 는 FE 입 니 다.RESET。이 함 수 는 foreach 가 시작 되 기 전에 배열 의 내부 지침 을 첫 번 째 요 소 를 가리 키 는 것 을 책임 집 니 다.

static int ZEND_FASTCALL  ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    zend_op *opline = EX(opline);
    zval *array_ptr, **array_ptr_ptr;
    HashTable *fe_ht;
    zend_object_iterator *iter = NULL;
    zend_class_entry *ce = NULL;
    zend_bool is_empty = 0;
    // FE_RESET
    if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {
        array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_R TSRMLS_CC);
        if (array_ptr_ptr == NULL || array_ptr_ptr == &EG(uninitialized_zval_ptr)) {
            ……
        }
        // foreach object
        else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) {
            ……
        }
        else {
            //
            if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) {
                // SEPARATE_ZVAL_IF_NOT_REF
                //
                // $tmp $arr, 2
                SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr);
                if (opline->extended_value & ZEND_FE_FETCH_BYREF) {
                    Z_SET_ISREF_PP(array_ptr_ptr);
                }
            }
            array_ptr = *array_ptr_ptr;
            Z_ADDREF_P(array_ptr);
        }
    } else {
        ……
    }

    //
    ……
}
코드 에서 알 수 있 듯 이 실제 실행 변수 분 리 는 할당 문 구 를 실행 할 때 가 아니 라 변 수 를 사용 할 때 로 미 루 었 다.이것 도 Copy On Write 체제 가 PHP 에서 이 루어 진 것 이다.FE_RESET 이후 메모리 의 변 화 는 다음 과 같다. 위의 그림 은 왜 foreach 가 원래 의$arr 에 영향 을 주지 않 는 지 설명 했다.에 대하 여 refcount 및 isref 의 변화 상황,관심 있 는 학생 은 ZEND 를 자세히 읽 을 수 있 습 니 다.FE_RESET_SPEC_CV_HANDLER 와 ZENDSWITCH_FREE_SPEC_VAR_HANDLER 의 구체 적 인 실현(모두 php-src/zend/zend 에 있 음vm_execute.h 중)본 고 는 상세 한 분석 을 하지 않 는 다.)

좋은 웹페이지 즐겨찾기